19 Angajamente c62a5550cc ... 61be33ce1f

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  “陈博论” 61be33ce1f 订单页面修改 9 luni în urmă
  “陈博论” 94926662de 支付宝下单 9 luni în urmă
  “陈博论” f11328f434 套餐分配修改 9 luni în urmă
  “陈博论” 0a5c108cc0 企业产品分配修改 9 luni în urmă
  “陈博论” d7f28b90b2 产品分配逻辑修改 9 luni în urmă
  PC-7\Administrator 82cc70d1dc 第一次大修改 9 luni în urmă
  “陈博论” 8348b1c8df 忘记密码页面修改 9 luni în urmă
  “陈博论” 27d0645c23 用户下单修改,判断有无企业 9 luni în urmă
  “陈博论” ba273bf0d7 前端样式更新 9 luni în urmă
  “陈博论” 5c5b034a0b 我的企业管理 9 luni în urmă
  “陈博论” c63f96b477 我的企业编写 9 luni în urmă
  “陈博论” f8e83144e3 我的下级列表编写 9 luni în urmă
  “陈博论” 3b9419a7b4 个人中心信息编写 9 luni în urmă
  “陈博论” 681195b448 订单支付页面编写 9 luni în urmă
  “陈博论” c80909f840 客户端首页api对接 9 luni în urmă
  “陈博论” c0e7be2d4a 登录注册修改 9 luni în urmă
  “陈博论” 5e0e766b8d 前端样式修改 10 luni în urmă
  “陈博论” 84ed9bcff4 前端打包更新 10 luni în urmă
  “陈博论” c69dfcd770 前端修改 10 luni în urmă
57 a modificat fișierele cu 12397 adăugiri și 2355 ștergeri
  1. 2 0
      app/admin/controller/package/Package.php
  2. 225 11
      app/api/controller/User.php
  3. 235 0
      app/api/controller/invitation/Allocation.php
  4. 597 0
      app/api/controller/invitation/Company.php
  5. 395 0
      app/api/controller/invitation/Order.php
  6. 164 0
      app/api/controller/invitation/Product.php
  7. 161 0
      app/api/controller/invitation/Withdrawal.php
  8. 161 0
      app/api/controller/product/OrderStatus.php
  9. 309 0
      app/api/controller/product/Package.php
  10. 744 0
      app/api/controller/product/Payment.php
  11. 290 0
      app/api/controller/product/Purchase.php
  12. 1 1
      app/api/validate/User.php
  13. 1 1
      app/common/library/Auth.php
  14. 314 0
      app/common/service/AlipayPaymentService.php
  15. 96 0
      app/common/service/BasePaymentService.php
  16. 504 0
      app/common/service/CommissionService.php
  17. 1414 0
      app/common/service/CompanyPackageService.php
  18. 342 0
      app/common/service/InvitationService.php
  19. 21 0
      app/common/service/PaymentFactory.php
  20. 390 0
      app/common/service/UserProductService.php
  21. 418 0
      app/common/service/WechatPaymentService.php
  22. 2 4
      web/src/api/backend/userCenter/user.ts
  23. 28 0
      web/src/api/frontend/home.ts
  24. 150 0
      web/src/api/frontend/me.ts
  25. 46 0
      web/src/api/frontend/order.ts
  26. 1 1
      web/src/router/static/memberCenterBase.ts
  27. 21 4
      web/src/views/backend/order/order/index.vue
  28. 19 10
      web/src/views/backend/order/withdrawal/index.vue
  29. 4 1
      web/src/views/backend/package/package/index.vue
  30. 7 2
      web/src/views/backend/package/package/popupForm.vue
  31. 11 2
      web/src/views/backend/package/package/specDialog.vue
  32. 10 0
      web/src/views/backend/package/product/index.vue
  33. 8 3
      web/src/views/backend/package/product/popupForm.vue
  34. 24 10
      web/src/views/backend/pay/accountConfig/index.vue
  35. 3 0
      web/src/views/backend/userCenter/index/index.vue
  36. 443 275
      web/src/views/backend/userCenter/user/detailDialog.vue
  37. 11 8
      web/src/views/backend/userCenter/user/index.vue
  38. 7 7
      web/src/views/backend/userCenter/user/popupForm.vue
  39. 26 7
      web/src/views/frontend/Me.vue
  40. 365 116
      web/src/views/frontend/Order.vue
  41. 83 85
      web/src/views/frontend/Register.vue
  42. 57 57
      web/src/views/frontend/Reset.vue
  43. 55 89
      web/src/views/frontend/components/AddMemberDialog.vue
  44. 34 49
      web/src/views/frontend/components/CreateEnterpriseDialog.vue
  45. 160 168
      web/src/views/frontend/components/Me/enterprise.vue
  46. 678 46
      web/src/views/frontend/components/Me/info.vue
  47. 231 175
      web/src/views/frontend/components/Me/purchaseHistory.vue
  48. 459 93
      web/src/views/frontend/components/Me/purchaseProduct.vue
  49. 118 120
      web/src/views/frontend/components/Me/subordinates.vue
  50. 793 0
      web/src/views/frontend/components/Me/withdrawList.vue
  51. 398 0
      web/src/views/frontend/components/PayQrCodeDialog.vue
  52. 496 282
      web/src/views/frontend/components/ProductSeatDialog.vue
  53. 94 10
      web/src/views/frontend/components/goods.vue
  54. 12 6
      web/src/views/frontend/components/header.vue
  55. 162 38
      web/src/views/frontend/components/success.vue
  56. 251 308
      web/src/views/frontend/index.vue
  57. 346 366
      web/src/views/frontend/user/login.vue

+ 2 - 0
app/admin/controller/package/Package.php

@@ -335,6 +335,7 @@ class Package extends Backend
         $unitValue = $this->request->post('unit_value', 0);
         $pointsValue = $this->request->post('points_value', 0);
         $description = $this->request->post('description', '');
+        $image = $this->request->post('image', '');
         $sort = $this->request->post('sort', 0);
 
         if (empty($packageId) || empty($specName)) {
@@ -382,6 +383,7 @@ class Package extends Backend
             'price' => $price,
             'stock' => $stock,
             'type' => $type,
+            'image' => $image,
             'card_type' => $cardType,
             'unit_value' => $unitValue,
             'points_value' => $pointsValue,

+ 225 - 11
app/api/controller/User.php

@@ -2,6 +2,9 @@
 
 namespace app\api\controller;
 
+use app\common\service\CommissionService;
+use app\common\service\InvitationService;
+use think\facade\Db;
 use Throwable;
 use ba\Captcha;
 use ba\ClickCaptcha;
@@ -12,11 +15,18 @@ use app\api\validate\User as UserValidate;
 
 class User extends Frontend
 {
-    protected array $noNeedLogin = ['checkIn', 'logout'];
+    protected array $noNeedLogin = ['*'];
+
+    protected $invitationService;
+    protected $commissionService;
 
     public function initialize(): void
     {
         parent::initialize();
+
+        $this->invitationService = new InvitationService();
+        $this->commissionService = new CommissionService();
+
     }
 
     /**
@@ -40,16 +50,21 @@ class User extends Frontend
         $userLoginCaptchaSwitch = Config::get('buildadmin.user_login_captcha');
 
         if ($this->request->isPost()) {
-            $params = $this->request->post(['tab', 'email', 'mobile', 'username', 'password', 'keep', 'captcha', 'captchaId', 'captchaInfo', 'registerType']);
+            $params = $this->request->post();
 
-            // 提前检查 tab ,然后将以 tab 值作为数据验证场景
-            if (!in_array($params['tab'] ?? '', ['login', 'register'])) {
+            // 检查必填的 tab 字段
+            if (!isset($params['tab']) || !in_array($params['tab'], ['login', 'register'])) {
                 $this->error(__('Unknown operation'));
             }
-
             $validate = new UserValidate();
             try {
-                $validate->scene($params['tab'])->check($params);
+                if ($params['tab'] == 'register') {
+                    $registerType = $params['registerType'] ?? 'email';
+
+                    $validate->scene('register_' . $registerType)->check($params);
+                } else {
+                    $validate->scene($params['tab'])->check($params);
+                }
             } catch (Throwable $e) {
                 $this->error($e->getMessage());
             }
@@ -57,23 +72,59 @@ class User extends Frontend
             if ($params['tab'] == 'login') {
                 if ($userLoginCaptchaSwitch) {
                     $captchaObj = new ClickCaptcha();
-                    if (!$captchaObj->check($params['captchaId'], $params['captchaInfo'])) {
+                    if (!isset($params['captchaId']) || !isset($params['captchaInfo']) ||
+                        !$captchaObj->check($params['captchaId'], $params['captchaInfo'])) {
                         $this->error(__('Captcha error'));
                     }
                 }
-                $res = $this->auth->login($params['username'], $params['password'], !empty($params['keep']));
+                $username = $params['username'] ?? '';
+                $password = $params['password'] ?? '';
+                $keep = !empty($params['keep']);
+
+                if (empty($username) || empty($password)) {
+                    $this->error(__('Please enter username and password'));
+                }
+
+                $res = $this->auth->login($username, $password, $keep);
             } elseif ($params['tab'] == 'register') {
+                $registerType = $params['registerType'] ?? 'email';
+                // 验证码验证
                 $captchaObj = new Captcha();
-                if (!$captchaObj->check($params['captcha'], $params[$params['registerType']] . 'user_register')) {
+                $captchaKey = $params[$registerType] ?? '';
+                if (empty($captchaKey) || !$captchaObj->check($params['captcha'] ?? '', $captchaKey . 'user_register')) {
                     $this->error(__('Please enter the correct verification code'));
                 }
-                $res = $this->auth->register($params['username'], $params['password'], $params['mobile'], $params['email']);
+
+                $username = $params['username'] ?? '';
+                $password = $params['password'] ?? '';
+                $mobile = $params['mobile'] ?? '';
+                $email = $params['email'] ?? '';
+                $invitationCode = $params['inviteCode'] ?? ''; // 用户输入的邀请码(别人的)
+                // 根据注册类型验证
+                if ($registerType == 'mobile' && empty($mobile)) {
+                    $this->error(__('Please enter mobile number'));
+                } elseif ($registerType == 'email' && empty($email)) {
+                    $this->error(__('Please enter email address'));
+                }
+
+                // 正常注册
+                $group = 0;
+                $res = $this->auth->register($username, $password, $mobile, $email, $group);
+
+                // 注册成功后处理邀请关系,并为用户生成自己的邀请码
+                if ($res === true) {
+                    $userId = $this->auth->id;
+                    if ($userId) {
+                        // 处理邀请关系(传入用户输入的邀请码)
+                        $this->invitationService->processRegistration($userId, $invitationCode);
+                    }
+                }
             }
 
             if (isset($res) && $res === true) {
                 $this->success(__('Login succeeded!'), [
                     'userInfo'  => $this->auth->getUserInfo(),
-                    'routePath' => '/user'
+                    'routePath' => '/'
                 ]);
             } else {
                 $msg = $this->auth->getError();
@@ -88,6 +139,169 @@ class User extends Frontend
         ]);
     }
 
+    /**
+     * 我的主页数据
+     */
+    public function dashboard(): void
+    {
+        // 确保用户已登录
+        if (!$this->auth->isLogin()) {
+            $this->error('请先登录');
+        }
+
+        $userId = $this->auth->id;
+        if (!$userId) {
+            $this->error('用户ID获取失败');
+        }
+
+        try {
+            // 1. 获取用户基本信息
+            $user = Db::table('ba_user')
+                ->where('id', $userId)
+                ->field('id, username, nickname, email, mobile, avatar, 
+                    last_login_time, join_time, join_ip, create_time')
+                ->find();
+
+            if (!$user) {
+                $this->error('用户信息不存在');
+            }
+
+            // 2. 获取邀请统计
+            $invitationService = new InvitationService();
+            $invitationStats = $invitationService->getUserInvitationStats($userId);
+
+            // 3. 获取佣金信息
+            $commissionService = new CommissionService();
+            $commissionDetail = $commissionService->getUserCommissionDetail($userId);
+
+            // 4. 格式化时间
+            $formatTime = function($timestamp) {
+                if (!$timestamp) return '';
+                if (is_numeric($timestamp)) {
+                    return date('Y-m-d H:i:s', $timestamp);
+                }
+                return $timestamp;
+            };
+
+            $response = [
+                // 用户基本信息
+                'user_info' => [
+                    'email' => $user['email'],
+                    'username' => $user['username'],
+                    'nickname' => $user['nickname'],
+                    'mobile' => $user['mobile'],
+                    'avatar' => $user['avatar'],
+                    'join_time' => $formatTime($user['join_time']),
+                    'create_time' => $formatTime($user['create_time']),
+                ],
+
+                // 登录信息
+                'login_info' => [
+                    'last_login_time' => $formatTime($user['last_login_time']),
+                    'current_login_time' => date('Y-m-d H:i:s'),
+                    'join_ip' => $user['join_ip'],
+                ],
+
+                // 推广信息
+                'promotion_info' => [
+                    'invitation_code' => $invitationStats['invitation_code'],
+                    'promotion_link' => $commissionDetail['commission']['promotion_link'],
+                    'invitation_count' => $invitationStats['invitation_count'],
+                    'total_invitation' => $invitationStats['commission']['total_invitation'] ?? 0,
+                ],
+
+                // 佣金信息
+                'commission_info' => [
+                    'current_commission' => $commissionDetail['commission']['current_commission'],
+                    'total_commission' => $commissionDetail['commission']['total_commission'],
+                    'withdrawn_commission' => $commissionDetail['commission']['withdrawn_commission'],
+                    'frozen_commission' => $commissionDetail['commission']['frozen_commission'],
+                    'today_income' => $commissionDetail['stats']['today_income'] ?? 0,
+                    'month_income' => $commissionDetail['stats']['month_income'] ?? 0,
+                ],
+
+                // 快速统计
+//                'quick_stats' => [
+//                    'total_orders' => $this->getUserOrderCount($userId),
+//                    'pending_withdrawals' => $this->getPendingWithdrawalCount($userId),
+//                ]
+            ];
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('获取我的主页数据失败: ' . $e->getMessage());
+            $this->error('获取数据失败');
+        }
+        $this->success('', $response);
+    }
+
+
+    /**
+     * 获取用户邀请统计
+     */
+    public function invitationStats(): void
+    {
+        $userId = $this->auth->id;
+
+        $stats = $this->invitationService->getUserInvitationStats($userId);
+
+        $this->success('', $stats);
+    }
+
+    /**
+     * 获取下级用户列表
+     */
+    public function subordinateList(): void
+    {
+        $userId = $this->auth->id;
+        $page = $this->request->get('page', 1);
+        $pageSize = $this->request->get('pageSize', 10);
+
+        $result = $this->invitationService->getSubordinateList($userId, $page, $pageSize);
+
+        $this->success('', $result);
+    }
+
+    /**
+     * 获取佣金详情
+     */
+    public function commissionDetail(): void
+    {
+        $userId = $this->auth->id;
+        $detail = $this->commissionService->getUserCommissionDetail($userId);
+
+        $this->success('', $detail);
+    }
+
+    /**
+     * 获取佣金流水
+     */
+    public function commissionFlow(): void
+    {
+        $userId = $this->auth->id;
+        $page = $this->request->get('page', 1);
+        $pageSize = $this->request->get('pageSize', 20);
+
+        try {
+            $list = \think\facade\Db::name('commission_flow')
+                ->where('user_id', $userId)
+                ->order('create_time', 'desc')
+                ->paginate([
+                    'page' => $page,
+                    'list_rows' => $pageSize
+                ]);
+        } catch (\Throwable $e) {
+            $this->error('获取佣金流水失败');
+        }
+
+        $this->success('', [
+            'list' => $list->items(),
+            'total' => $list->total(),
+            'page' => $list->currentPage(),
+            'page_size' => $list->listRows(),
+            'page_count' => $list->lastPage()
+        ]);
+    }
+
     public function logout(): void
     {
         if ($this->request->isPost()) {

+ 235 - 0
app/api/controller/invitation/Allocation.php

@@ -0,0 +1,235 @@
+<?php
+namespace app\api\controller\invitation;
+
+use app\common\controller\Frontend;
+use app\common\service\CompanyPackageService;
+use think\facade\Db;
+
+class Allocation extends Frontend
+{
+    protected array $noNeedPermission = ['allocationList', 'assign', 'cancel'];
+    protected $companyPackageService;
+
+    public function initialize(): void
+    {
+        parent::initialize();
+        $this->companyPackageService = new CompanyPackageService();
+    }
+
+    /**
+     * 获取可分配的企业套餐列表(区分类型)
+     */
+    public function allocationList()
+    {
+        $userId = $this->auth->id;
+
+        // 获取用户所在企业
+        $companyService = new CompanyPackageService();
+        $companyInfo = $companyService->getUserCompany($userId);
+        if (!$companyInfo) {
+            $this->error('您不属于任何企业');
+        }
+
+        $result = $this->companyPackageService->getAvailableCompanyPackages(
+            $companyInfo['id'],
+            $userId
+        );
+
+        $this->success('', $result);
+    }
+
+    /**
+     * 分配套餐给企业成员(统一处理所有卡类型)
+     */
+    public function assign()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('非法请求');
+        }
+
+        $userId = $this->auth->id;
+        $seatsId = $this->request->post('seats_id', 0);
+        $assignedToUserId = $this->request->post('assigned_to_user_id', 0);
+        $remark = $this->request->post('remark', '');
+
+        if ($seatsId <= 0 || $assignedToUserId <= 0) {
+            $this->error('参数错误');
+        }
+
+        try {
+            // 获取席位信息以确定卡类型
+            $seats = Db::table('ba_company_package_seats')
+                ->where('id', $seatsId)
+                ->find();
+
+            if (!$seats) {
+                $this->error('企业套餐不存在');
+            }
+
+            if ($seats['card_type'] >= 3) {
+                // 日期卡:使用原有的分配逻辑
+                $result = $this->companyPackageService->allocatePackage(
+                    $seatsId,
+                    $assignedToUserId,
+                    $userId,
+                    $remark
+                );
+            } else {
+                // 次卡/积分卡:使用新的权限分配逻辑
+                $result = $this->companyPackageService->assignPackagePermission(
+                    $seatsId,
+                    $assignedToUserId,
+                    $userId,
+                    $remark
+                );
+            }
+
+        } catch (\Throwable $e) {
+            $this->error($e->getMessage());
+        }
+
+        if ($result['success'] ?? $result) {
+            $this->success('分配成功');
+        } else {
+            $this->error('分配失败');
+        }
+    }
+
+    /**
+     * 取消分配
+     */
+    public function cancel()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('非法请求');
+        }
+
+        $userId = $this->auth->id;
+        $allocationId = $this->request->post('allocation_id', 0);
+        $remark = $this->request->post('remark', '');
+
+        if ($allocationId <= 0) {
+            $this->error('参数错误');
+        }
+
+        try {
+            // 获取分配记录以确定卡类型
+            $allocation = Db::table('ba_company_package_allocation')
+                ->alias('cpa')
+                ->leftJoin('ba_package_price pp', 'pp.id = cpa.package_price_id')
+                ->where('cpa.id', $allocationId)
+                ->field('cpa.*, pp.card_type')
+                ->find();
+;
+            if (!$allocation) {
+                $this->error('分配记录不存在');
+            }
+
+            if ($allocation['card_type'] >= 3) {
+                // 日期卡:使用原有的取消分配逻辑
+                $result = $this->companyPackageService->cancelAllocation(
+                    $allocationId,
+                    $userId,
+                    $remark
+                );
+            } else {
+                // 次卡/积分卡:使用新的权限取消逻辑
+                $result = $this->companyPackageService->cancelPackagePermission(
+                    $allocationId,
+                    $userId,
+                    $remark
+                );
+            }
+
+
+        } catch (\Throwable $e) {
+            $this->error($e->getMessage());
+        }
+        if ($result) {
+            $this->success('取消分配成功');
+        } else {
+            $this->error('取消分配失败');
+        }
+    }
+
+    /**
+     * 获取企业成员已分配的套餐
+     */
+    public function myAssignedPackages()
+    {
+        $userId = $this->auth->id;
+
+            $allocations = Db::name('ba_company_package_allocation')
+                ->alias('cpa')
+                ->leftJoin('ba_product p', 'p.id = cpa.product_id')
+                ->leftJoin('ba_package_price pp', 'pp.id = cpa.package_price_id')
+                ->leftJoin('ba_user u', 'u.id = cpa.assigned_by_user_id')
+                ->where('cpa.assigned_to_user_id', $userId)
+                ->where('cpa.allocation_status', 1) // 已分配
+                ->field('cpa.*, 
+                    p.name as product_name, p.type as product_type, p.link,
+                    pp.spec_name, pp.type as package_type, pp.card_type,
+                    u.nickname as assigned_by_name')
+                ->select()
+                ->toArray();
+
+            $result = [];
+            foreach ($allocations as $allocation) {
+                $result[] = [
+                    'allocation_id' => $allocation['id'],
+                    'product_name' => $allocation['product_name'],
+                    'product_type' => $allocation['product_type'],
+                    'product_link' => $allocation['link'],
+                    'spec_name' => $allocation['spec_name'],
+                    'package_type' => $allocation['package_type'],
+                    'card_type' => $allocation['card_type'],
+                    'card_type_text' => $this->companyPackageService->getCardTypeText($allocation['card_type']),
+                    'end_time' => $allocation['end_time'],
+                    'remaining_times' => $allocation['remaining_times'],
+                    'remaining_points' => $allocation['remaining_points'],
+                    'assigned_by' => $allocation['assigned_by_name'],
+                    'allocation_time' => $allocation['allocation_time']
+                ];
+            }
+
+            $this->success('', $result);
+    }
+
+
+    /**
+     * 使用企业次卡/积分卡
+     */
+    public function useSharedPackage()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('非法请求');
+        }
+
+        $userId = $this->auth->id;
+        $packagePriceId = $this->request->post('package_price_id', 0);
+        $quantity = $this->request->post('quantity', 1);
+        $remark = $this->request->post('remark', '');
+
+        if ($packagePriceId <= 0 || $quantity <= 0) {
+            $this->error('参数错误');
+        }
+
+            $result = $this->companyPackageService->useSharedCompanyPackage(
+                $userId,
+                $packagePriceId,
+                $quantity,
+                null,
+                $remark
+            );
+
+            if ($result['success']) {
+                $this->success($result['message'], [
+                    'remaining_times' => $result['remaining_times'],
+                    'remaining_points' => $result['remaining_points']
+                ]);
+            } else {
+                $this->error('使用失败');
+            }
+
+    }
+}

+ 597 - 0
app/api/controller/invitation/Company.php

@@ -0,0 +1,597 @@
+<?php
+
+namespace app\api\controller\invitation;
+
+use app\common\controller\Frontend;
+use think\facade\Db;
+
+class Company extends Frontend
+{
+    protected array $noNeedPermission = ['myCompanies', 'members', 'addMember', 'removeMember', 'createCompany', 'updateCompany', 'deleteCompany','searchEmail','checkCompany'];
+
+
+    /**
+     * 检查当前用户是否已有企业
+     */
+    public function checkCompany(): void
+    {
+            $userId = $this->auth->id;
+            if (!$userId) {
+                $this->error('用户未登录');
+            }
+
+            $hasCompany = $this->hasCompany($userId);
+            $companyInfo = $hasCompany ? $this->getUserCompany($userId) : null;
+
+            $this->success('', [
+                'has_company' => $hasCompany,
+                'company' => $companyInfo,
+                'message' => $hasCompany
+                    ? '您已' . ($companyInfo['role'] == 'creator' ? '创建' : '加入') . '企业《' . ($companyInfo['name'] ?? '') . '》'
+                    : '您尚未创建或加入任何企业'
+            ]);
+    }
+
+    /**
+     * 检查用户是否已有企业(创建或加入)
+     * @return bool 返回true表示已有企业,false表示没有企业
+     */
+    private function hasCompany($userId): bool
+    {
+        try {
+            // 1. 检查是否创建了企业
+            $createdCompany = Db::table('ba_company')
+                ->where('creator_id', $userId)
+                ->where('status', 1) // 只检查正常状态的企业
+                ->count();
+
+            if ($createdCompany > 0) {
+                return true;
+            }
+
+            // 2. 检查是否加入了企业(作为成员)
+            $joinedCompany = Db::table('ba_company_member')
+                ->alias('cm')
+                ->leftJoin('ba_company c', 'c.id = cm.company_id')
+                ->where('cm.user_id', $userId)
+                ->where('cm.status', 1) // 成员状态正常
+                ->where('c.status', 1) // 企业状态正常
+                ->count();
+
+            return $joinedCompany > 0;
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('检查用户是否已有企业失败: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+
+    /**
+     * 获取用户的唯一企业信息(如果有)
+     * @return array|null 返回企业信息,没有则返回null
+     */
+    private function getUserCompany($userId): ?array
+    {
+        try {
+            // 先检查是否是创建人
+            $createdCompany = Db::table('ba_company')
+                ->where('creator_id', $userId)
+                ->where('status', 1)
+                ->field('id, name, creator_id, creator_name, status')
+                ->find();
+
+            if ($createdCompany) {
+                $createdCompany['role'] = 'creator'; // 角色:创建人
+                return $createdCompany;
+            }
+
+            // 再检查是否是成员
+            $joinedCompany = Db::table('ba_company_member')
+                ->alias('cm')
+                ->leftJoin('ba_company c', 'c.id = cm.company_id')
+                ->where('cm.user_id', $userId)
+                ->where('cm.status', 1)
+                ->where('c.status', 1)
+                ->field('c.id, c.name, cm.role, cm.name as member_name, cm.email as member_email, cm.join_time')
+                ->find();
+
+            if ($joinedCompany) {
+                $joinedCompany['role'] = $joinedCompany['role'] == 1 ? 'admin' : 'member'; // 管理员或普通成员
+                return $joinedCompany;
+            }
+
+            return null;
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('获取用户企业信息失败: ' . $e->getMessage());
+            return null;
+        }
+    }
+
+
+    /**
+     * 获取我的企业列表(我创建的企业)
+     */
+    public function myCompanies(): void
+    {
+
+            $userId = $this->auth->id;
+            if (!$userId) {
+                $this->error('用户未登录');
+            }
+
+            // 获取用户的唯一企业
+            $companyInfo = $this->getUserCompany($userId);
+
+            if (!$companyInfo) {
+                $this->success('您尚未创建或加入任何企业', [
+                    'has_company' => false,
+                    'company' => null,
+                    'message' => '您可以创建一个新企业,或者请其他企业管理员邀请您加入'
+                ]);
+            }
+
+            // 获取企业成员数量
+            $memberCount = Db::table('ba_company_member')
+                ->where('company_id', $companyInfo['id'])
+                ->where('status', 1)
+                ->count();
+
+            $companyInfo['member_count'] = $memberCount;
+            $companyInfo['has_company'] = true;
+
+            // 格式化角色文本
+            if (isset($companyInfo['role'])) {
+                switch ($companyInfo['role']) {
+                    case 'creator':
+                        $companyInfo['role_text'] = '企业创建人';
+                        break;
+                    case 'admin':
+                        $companyInfo['role_text'] = '管理员';
+                        break;
+                    case 'member':
+                        $companyInfo['role_text'] = '普通成员';
+                        break;
+                    default:
+                        $companyInfo['role_text'] = '未知角色';
+                }
+            }
+
+            // 格式化时间
+            if (isset($companyInfo['join_time'])) {
+                $companyInfo['join_time_formatted'] = date('Y-m-d H:i', strtotime($companyInfo['join_time']));
+            }
+
+            if (isset($companyInfo['create_time'])) {
+                $companyInfo['create_time_formatted'] = date('Y-m-d H:i', strtotime($companyInfo['create_time']));
+            }
+
+            $this->success('', $companyInfo);
+
+
+
+    }
+
+    /**
+     * 获取企业成员列表
+     */
+    public function members(): void
+    {
+            $userId = $this->auth->id;
+            $companyId = $this->request->get('company_id', 0);
+            $page = $this->request->get('page', 1);
+            $pageSize = $this->request->get('pageSize', 10);
+
+            if ($companyId <= 0) {
+                $this->error('请选择企业');
+            }
+
+            // 验证用户是否有权限查看该企业
+            $company = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->field('id, creator_id')
+                ->find();
+
+            if (!$company) {
+                $this->error('企业不存在');
+            }
+            // 检查用户是否是企业创建人或成员
+            $isCreator = $company['creator_id'] == $userId;
+            $isMember = Db::table('ba_company_member')
+                ->where('company_id', $companyId)
+                ->where('user_id', $userId)
+                ->find();
+            if (!$isCreator && !$isMember) {
+                $this->error('无权查看该企业');
+            }
+
+            // 查询成员列表
+            $query = Db::table('ba_company_member')
+                ->where('company_id', $companyId);
+
+            $total = $query->count();
+            $members = $query->field('id, name, email, role, status, join_time, create_time')
+                ->order('role', 'asc')
+                ->order('join_time', 'asc')
+                ->page($page, $pageSize)
+                ->select()
+                ->toArray();
+
+            // 处理成员数据
+            foreach ($members as &$member) {
+                $member['role_text'] = $member['role'] == 1 ? '创建人/管理员' : '普通成员';
+                $member['status_text'] = $member['status'] == 1 ? '正常' : '禁用';
+
+                // 是否可以操作(只有创建人可以操作)
+                $member['can_operate'] = $isCreator && $member['role'] != 1; // 创建人不能操作自己
+            }
+
+            $this->success('', [
+                'list' => $members,
+                'total' => $total,
+                'page' => $page,
+                'page_size' => $pageSize,
+                'is_creator' => $isCreator // 前端根据这个判断是否显示操作按钮
+            ]);
+
+    }
+
+    /**
+     * 通过邮箱添加成员
+     */
+    public function addMember(): void
+    {
+
+            if (!$this->request->isPost()) {
+                $this->error('非法请求');
+            }
+
+            $userId = $this->auth->id;
+            $companyId = $this->request->post('company_id', 0);
+            $email = $this->request->post('email', '');
+            $name = $this->request->post('name', '');
+
+            if ($companyId <= 0) {
+                $this->error('请选择企业');
+            }
+
+            if (empty($email) || empty($name)) {
+                $this->error('邮箱和姓名不能为空');
+            }
+
+            // 验证邮箱格式
+            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+                $this->error('邮箱格式不正确');
+            }
+
+            // 验证用户是否有权限添加成员(必须是企业创建人)
+            $company = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->where('creator_id', $userId)
+                ->find();
+
+            if (!$company) {
+                $this->error('无权添加成员或企业不存在');
+            }
+
+            // 检查邮箱是否已经是成员
+            $exists = Db::table('ba_company_member')
+                ->where('company_id', $companyId)
+                ->where('email', $email)
+                ->find();
+
+            if ($exists) {
+                $this->error('该邮箱已经是企业成员');
+            }
+
+            // 查询邮箱对应的用户ID(如果用户已注册)
+            $user = Db::table('ba_user')
+                ->where('email', $email)
+                ->field('id')
+                ->find();
+
+            $userIdForMember = $user ? $user['id'] : null;
+
+            // 添加成员
+            $memberData = [
+                'company_id' => $companyId,
+                'user_id' => $userIdForMember,
+                'email' => $email,
+                'name' => $name,
+                'role' => 2, // 普通成员
+                'status' => 1,
+                'join_time' => date('Y-m-d H:i:s'),
+                'create_time' => date('Y-m-d H:i:s'),
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            $result = Db::table('ba_company_member')->insert($memberData);
+
+            if ($result) {
+                $this->success('添加成员成功');
+            } else {
+                $this->error('添加成员失败');
+            }
+
+    }
+
+    /**
+     * 移除企业成员
+     */
+    public function removeMember(): void
+    {
+
+            if (!$this->request->isPost()) {
+                $this->error('非法请求');
+            }
+
+            $userId = $this->auth->id;
+            $memberId = $this->request->post('member_id', 0);
+
+            if ($memberId <= 0) {
+                $this->error('成员ID错误');
+            }
+
+            // 查询成员信息
+            $member = Db::table('ba_company_member')
+                ->where('id', $memberId)
+                ->find();
+
+            if (!$member) {
+                $this->error('成员不存在');
+            }
+
+            // 验证用户是否有权限移除成员(必须是企业创建人)
+            $company = Db::table('ba_company')
+                ->where('id', $member['company_id'])
+                ->where('creator_id', $userId)
+                ->find();
+
+            if (!$company) {
+                $this->error('无权移除成员');
+            }
+
+            // 不能移除创建人自己
+            if ($member['role'] == 1) {
+                $this->error('不能移除创建人');
+            }
+
+            // 移除成员
+            $result = Db::table('ba_company_member')
+                ->where('id', $memberId)
+                ->delete();
+
+            if ($result) {
+                $this->success('移除成员成功');
+            } else {
+                $this->error('移除成员失败');
+            }
+
+    }
+
+    /**
+     * 创建企业
+     */
+    public function createCompany(): void
+    {
+
+            if (!$this->request->isPost()) {
+                $this->error('非法请求');
+            }
+
+            $userId = $this->auth->id;
+            $name = $this->request->post('name', '');
+
+            if (empty($name)) {
+                $this->error('企业名称不能为空');
+            }
+
+
+        if ($this->hasCompany($userId)) {
+            $companyInfo = $this->getUserCompany($userId);
+            $role = isset($companyInfo['role']) ? $companyInfo['role'] : '';
+            $companyName = isset($companyInfo['name']) ? $companyInfo['name'] : '';
+
+            $message = "您已经";
+            if ($role == 'creator') {
+                $message .= "创建了企业《{$companyName}》";
+            } elseif ($role == 'admin') {
+                $message .= "加入了企业《{$companyName}》并拥有管理员权限";
+            } elseif ($role == 'member') {
+                $message .= "加入了企业《{$companyName}》";
+            }
+            $message .= ",无法再创建新企业。";
+
+            $this->error($message);
+        }
+
+            // 检查企业名称是否已存在
+            $exists = Db::table('ba_company')
+                ->where('name', $name)
+                ->find();
+
+            if ($exists) {
+                $this->error('企业名称已存在');
+            }
+
+            // 获取当前用户信息
+            $user = Db::table('ba_user')
+                ->where('id', $userId)
+                ->field('username, nickname')
+                ->find();
+
+            $creatorName = $user['nickname'] ?: $user['username'];
+
+            // 创建企业
+            $companyData = [
+                'name' => $name,
+                'creator_id' => $userId,
+                'creator_name' => $creatorName,
+                'status' => 1,
+                'create_time' => date('Y-m-d H:i:s'),
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            $companyId = Db::table('ba_company')->insertGetId($companyData);
+
+            if ($companyId) {
+                // 将创建人自己添加到企业成员表中
+                $memberData = [
+                    'company_id' => $companyId,
+                    'user_id' => $userId,
+                    'email' => $this->auth->email ?: '',
+                    'name' => $creatorName,
+                    'role' => 1, // 创建人/管理员
+                    'status' => 1,
+                    'join_time' => date('Y-m-d H:i:s'),
+                    'create_time' => date('Y-m-d H:i:s'),
+                    'update_time' => date('Y-m-d H:i:s')
+                ];
+
+                Db::table('ba_company_member')->insert($memberData);
+
+                $this->success('创建企业成功', [
+                    'company_id' => $companyId,
+                    'name' => $name
+                ]);
+            } else {
+                $this->error('创建企业失败');
+            }
+
+    }
+
+    /**
+     * 更新企业信息
+     */
+    public function updateCompany(): void
+    {
+
+            if (!$this->request->isPost()) {
+                $this->error('非法请求');
+            }
+
+            $userId = $this->auth->id;
+            $companyId = $this->request->post('company_id', 0);
+            $name = $this->request->post('name', '');
+
+            if ($companyId <= 0) {
+                $this->error('企业ID错误');
+            }
+
+            if (empty($name)) {
+                $this->error('企业名称不能为空');
+            }
+
+            // 验证用户是否有权限修改(必须是企业创建人)
+            $company = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->where('creator_id', $userId)
+                ->find();
+
+            if (!$company) {
+                $this->error('无权修改企业信息或企业不存在');
+            }
+
+            // 检查企业名称是否与其他企业重名(排除自己)
+            $exists = Db::table('ba_company')
+                ->where('name', $name)
+                ->where('id', '<>', $companyId)
+                ->find();
+
+            if ($exists) {
+                $this->error('企业名称已存在');
+            }
+
+            // 更新企业信息
+            $result = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->update([
+                    'name' => $name,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if ($result !== false) {
+                $this->success('更新企业信息成功');
+            } else {
+                $this->error('更新企业信息失败');
+            }
+
+    }
+
+    /**
+     * 删除企业
+     */
+    public function deleteCompany(): void
+    {
+        try {
+            if (!$this->request->isPost()) {
+                $this->error('非法请求');
+            }
+
+            $userId = $this->auth->id;
+            $companyId = $this->request->post('company_id', 0);
+
+            if ($companyId <= 0) {
+                $this->error('企业ID错误');
+            }
+
+            // 验证用户是否有权限删除(必须是企业创建人)
+            $company = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->where('creator_id', $userId)
+                ->find();
+
+            if (!$company) {
+                $this->error('无权删除企业或企业不存在');
+            }
+
+            // 删除企业(会自动删除关联的成员,因为有外键约束)
+            $result = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->delete();
+
+            if ($result) {
+                $this->success('删除企业成功');
+            } else {
+                $this->error('删除企业失败');
+            }
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('删除企业失败: ' . $e->getMessage());
+            $this->error('删除企业失败');
+        }
+    }
+
+    /**
+     * 搜索邮箱用户(用于添加成员时搜索)
+     */
+    public function searchEmail(): void
+    {
+
+            $keyword = $this->request->get('keyword', '');
+
+            if (empty($keyword)) {
+                $this->success('', []);
+            }
+
+            // 搜索用户表,查找邮箱匹配的用户
+            $users = Db::table('ba_user')
+                ->whereLike('email', "%{$keyword}%")
+                ->field('id, username, nickname, email')
+                ->limit(10)
+                ->select()
+                ->toArray();
+
+            // 格式化返回数据
+            $result = [];
+            foreach ($users as $user) {
+                $result[] = [
+                    'id' => $user['id'],
+                    'name' => $user['nickname'] ?: $user['username'],
+                    'email' => $user['email']
+                ];
+            }
+
+            $this->success('', $result);
+
+    }
+}

+ 395 - 0
app/api/controller/invitation/Order.php

@@ -0,0 +1,395 @@
+<?php
+
+namespace app\api\controller\invitation;
+
+use app\common\controller\Frontend;
+use app\common\service\CommissionService;
+use think\facade\Db;
+
+class Order extends Frontend
+{
+    protected array $noNeedPermission = ['purchaseRecordsSimple','orderDetail'];
+    protected $commissionService;
+
+    public function initialize(): void
+    {
+        parent::initialize();
+        $this->commissionService = new CommissionService();
+    }
+
+
+    /**
+     * 获取用户购买记录(简化版)
+     */
+    public function purchaseRecordsSimple(): void
+    {
+        // 确保用户已登录
+        if (!$this->auth->isLogin()) {
+            $this->error('请先登录');
+        }
+
+        $userId = $this->auth->id;
+        if (!$userId) {
+            $this->error('用户ID获取失败');
+        }
+
+//        try {
+            // 获取查询参数
+            $page = $this->request->get('page', 1);
+            $pageSize = $this->request->get('pageSize', 10);
+            $status = $this->request->get('status', 'all'); // all, paid, unpaid
+
+            // 构建查询条件
+            $query = Db::table('ba_order')
+                ->where('user_id', $userId);
+
+            // 根据状态筛选
+            if ($status === 'paid') {
+                $query->where('pay_status', 1); // 已支付
+            } elseif ($status === 'unpaid') {
+                $query->where('pay_status', 0); // 未支付
+            }
+
+            // 获取订单总数
+            $total = $query->count();
+
+            // 获取订单列表(只取必要字段)
+            $orders = $query->field('order_no, pay_amount, pay_method, pay_status, order_time, pay_time')
+                ->order('order_time', 'desc')
+                ->page($page, $pageSize)
+                ->select()
+                ->toArray();
+            // 处理每个订单的商品信息
+            $orderNos = array_column($orders, 'order_no');
+
+            if (!empty($orderNos)) {
+                // 批量查询订单商品
+                $orderItems = \think\facade\Db::table('ba_order_item')
+                    ->where('order_id', 'IN', Db::table('ba_order')
+                        ->where('order_no', 'IN', $orderNos)
+                        ->column('id'))
+                    ->field('order_id, item_name, quantity, unit_price, total_price')
+                    ->select()
+                    ->toArray();
+
+                // 按order_id分组商品
+                $itemsByOrderId = [];
+                foreach ($orderItems as $item) {
+                    $itemsByOrderId[$item['order_id']][] = $item;
+                }
+
+                // 获取订单ID映射
+                $orderIdMap = \think\facade\Db::table('ba_order')
+                    ->where('order_no', 'IN', $orderNos)
+                    ->column('id', 'order_no');
+
+                // 合并商品信息到订单
+                foreach ($orders as &$order) {
+                    // 注意:现在 $orderIdMap 的键是 order_no,值是对应的 id
+                    $orderId = $orderIdMap[$order['order_no']] ?? 0;
+
+                    // 取第一个商品作为主商品信息(根据您的截图,一单一个商品)
+                    $mainItem = $itemsByOrderId[$orderId][0] ?? [];
+
+                    // 商品名称(如果有多个商品,显示第一个)
+                    $order['product_name'] = $mainItem['item_name'] ?? '商品';
+
+                    // 数量
+                    $order['quantity'] = $mainItem['quantity'] ?? 1;
+
+                    // 格式化显示字段
+                    $order['amount'] = '¥' . number_format($order['pay_amount'], 2);
+                    $order['pay_method_text'] = $this->getSimplePayMethodText($order['pay_method']);
+                    $order['pay_status_text'] = $order['pay_status'] == 1 ? '已支付' : '未支付';
+                    $order['order_time_formatted'] = $order['order_time'] ? date('Y-m-d H:i:s', strtotime($order['order_time'])) : '';
+                    $order['pay_time_formatted'] = $order['pay_time'] ? date('Y-m-d H:i:s', strtotime($order['pay_time'])) : '';
+
+                    // 移除不需要的原始字段
+                    unset($order['pay_amount'], $order['pay_method'], $order['pay_status'],
+                        $order['order_time'], $order['pay_time']);
+                }
+            }
+
+            // 获取简单的统计信息
+            $stats = [
+                'total_orders' => $total,
+                'paid_orders' =>Db::table('ba_order')
+                    ->where('user_id', $userId)
+                    ->where('pay_status', 1)
+                    ->count(),
+                'unpaid_orders' =>Db::table('ba_order')
+                    ->where('user_id', $userId)
+                    ->where('pay_status', 0)
+                    ->count(),
+            ];
+
+//        } catch (\Throwable $e) {
+//            \think\facade\Log::error('获取购买记录失败: ' . $e->getMessage());
+//            $this->error('获取购买记录失败');
+//        }
+        $this->success('', [
+            'list' => $orders,
+            'total' => $total,
+            'page' => $page,
+            'page_size' => $pageSize,
+            'stats' => $stats
+        ]);
+    }
+
+    /**
+     * 简化支付方式文本
+     */
+    private function getSimplePayMethodText(string $method): string
+    {
+        $methodMap = [
+            'wechat' => '微信',
+            'alipay' => '支付宝',
+            'bank' => '银行卡'
+        ];
+
+        return $methodMap[$method] ?? '其他';
+    }
+
+    /**
+     * 获取购买统计信息
+     */
+    private function getPurchaseStats(int $userId, string $status = 'all', string $startDate = '', string $endDate = ''): array
+    {
+        try {
+            // 全部订单统计
+            $totalQuery = \think\facade\Db::table('ba_order')
+                ->where('user_id', $userId);
+
+            // 已支付订单统计
+            $paidQuery = \think\facade\Db::table('ba_order')
+                ->where('user_id', $userId)
+                ->where('pay_status', 1);
+
+            // 时间范围筛选
+            if (!empty($startDate)) {
+                $totalQuery->where('order_time', '>=', $startDate);
+                $paidQuery->where('order_time', '>=', $startDate);
+            }
+            if (!empty($endDate)) {
+                $totalQuery->where('order_time', '<=', $endDate . ' 23:59:59');
+                $paidQuery->where('order_time', '<=', $endDate . ' 23:59:59');
+            }
+
+            $totalCount = $totalQuery->count();
+            $paidCount = $paidQuery->count();
+            $unpaidCount = $totalCount - $paidCount;
+
+            // 支付总金额
+            $totalAmount = $paidQuery->sum('pay_amount');
+            $totalAmount = $totalAmount ?: 0;
+
+            // 本月统计
+            $monthStart = date('Y-m-01');
+            $monthPaidAmount = \think\facade\Db::table('ba_order')
+                ->where('user_id', $userId)
+                ->where('pay_status', 1)
+                ->where('order_time', '>=', $monthStart)
+                ->sum('pay_amount');
+
+            $monthPaidAmount = $monthPaidAmount ?: 0;
+
+            return [
+                'total_orders' => $totalCount,
+                'paid_orders' => $paidCount,
+                'unpaid_orders' => $unpaidCount,
+                'total_amount' => $totalAmount,
+                'month_amount' => $monthPaidAmount
+            ];
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('获取购买统计失败: ' . $e->getMessage());
+            return [
+                'total_orders' => 0,
+                'paid_orders' => 0,
+                'unpaid_orders' => 0,
+                'total_amount' => 0,
+                'month_amount' => 0
+            ];
+        }
+    }
+
+    /**
+     * 获取支付状态文本
+     */
+    private function getPayStatusText(int $status): string
+    {
+        $statusMap = [
+            0 => '待支付',
+            1 => '已支付',
+            2 => '支付失败'
+        ];
+
+        return $statusMap[$status] ?? '未知状态';
+    }
+
+    /**
+     * 获取订单状态文本
+     */
+    private function getOrderStatusText(int $status): string
+    {
+        $statusMap = [
+            0 => '待处理',
+            1 => '已确认',
+            2 => '已发货',
+            3 => '已完成',
+            4 => '已取消'
+        ];
+
+        return $statusMap[$status] ?? '未知状态';
+    }
+
+    /**
+     * 获取支付方式文本
+     */
+    private function getPayMethodText(string $method): string
+    {
+        $methodMap = [
+            'wechat' => '微信支付',
+            'alipay' => '支付宝',
+            'bank' => '银行卡',
+            'balance' => '余额支付'
+        ];
+
+        return $methodMap[$method] ?? '其他支付';
+    }
+
+    /**
+     * 获取订单详情(单个订单)
+     */
+    public function orderDetail(): void
+    {
+        if (!$this->auth->isLogin()) {
+            $this->error('请先登录');
+        }
+
+        $userId = $this->auth->id;
+        $orderId = $this->request->get('order_id', 0);
+        $orderNo = $this->request->get('order_no', '');
+
+        if (empty($orderId) && empty($orderNo)) {
+            $this->error('参数错误');
+        }
+        try {
+            // 查询订单
+            if ($orderId) {
+                $order = \think\facade\Db::table('ba_order')
+                    ->where('id', $orderId)
+                    ->where('user_id', $userId) // 只能查看自己的订单
+                    ->find();
+            } else {
+                $order = \think\facade\Db::table('ba_order')
+                    ->where('order_no', $orderNo)
+                    ->where('user_id', $userId) // 只能查看自己的订单
+                    ->find();
+            }
+
+            if (!$order) {
+                $this->error('订单不存在或无权查看');
+            }
+
+            // 获取订单商品项
+            $orderItems = \think\facade\Db::table('ba_order_item')
+                ->where('order_id', $order['id'])
+                ->select()
+                ->toArray();
+
+            // 格式化显示数据
+            $order['pay_status_text'] = $this->getPayStatusText($order['pay_status']);
+            $order['order_status_text'] = $this->getOrderStatusText($order['order_status']);
+            $order['pay_method_text'] = $this->getPayMethodText($order['pay_method']);
+
+            // 格式化时间
+            $order['order_time_formatted'] = $order['order_time'] ? date('Y-m-d H:i:s', strtotime($order['order_time'])) : '';
+            $order['pay_time_formatted'] = $order['pay_time'] ? date('Y-m-d H:i:s', strtotime($order['pay_time'])) : '';
+            $order['complete_time_formatted'] = $order['complete_time'] ? date('Y-m-d H:i:s', strtotime($order['complete_time'])) : '';
+            $order['create_time_formatted'] = $order['create_time'] ? date('Y-m-d H:i:s', strtotime($order['create_time'])) : '';
+
+
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('获取订单详情失败: ' . $e->getMessage());
+            $this->error('获取订单详情失败');
+        }
+        $this->success('', [
+            'order' => $order,
+            'items' => $orderItems
+        ]);
+    }
+
+
+
+
+
+    /**
+     * 订单支付成功回调(处理佣金)
+     */
+    public function paymentCallback(): void
+    {
+        if (!$this->request->isPost()) {
+            $this->error('非法请求');
+        }
+
+        $orderId = $this->request->post('order_id', 0);
+        $orderNo = $this->request->post('order_no', '');
+
+        if (empty($orderId) && empty($orderNo)) {
+            $this->error('参数错误');
+        }
+
+        try {
+            // 查找订单
+            if (!empty($orderId)) {
+                $order = \think\facade\Db::table('ba_order')
+                    ->where('id', $orderId)
+                    ->find();
+            } else {
+                $order = \think\facade\Db::table('ba_order')
+                    ->where('order_no', $orderNo)
+                    ->find();
+            }
+
+            if (!$order) {
+                $this->error('订单不存在');
+            }
+
+            // 处理订单佣金
+            $result = $this->commissionService->processOrderCommission($order['id']);
+
+            if ($result) {
+                $this->success('佣金处理成功');
+            } else {
+                $this->error('佣金处理失败');
+            }
+        } catch (\Throwable $e) {
+            $this->error('处理失败:' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 手动触发订单佣金处理(管理用)
+     */
+    public function processCommission(): void
+    {
+        if (!$this->auth->isLogin() || !$this->auth->isAdmin()) {
+            $this->error('权限不足');
+        }
+
+        $orderId = $this->request->post('order_id', 0);
+
+        if ($orderId <= 0) {
+            $this->error('订单ID错误');
+        }
+
+        $result = $this->commissionService->processOrderCommission($orderId);
+
+        if ($result) {
+            $this->success('处理成功');
+        } else {
+            $this->error('处理失败');
+        }
+    }
+}

+ 164 - 0
app/api/controller/invitation/Product.php

@@ -0,0 +1,164 @@
+<?php
+namespace app\api\controller\invitation;
+
+use app\common\controller\Frontend;
+use app\common\service\UserProductService;
+use think\facade\Db;
+
+class Product extends Frontend
+{
+    protected array $noNeedPermission = ['purchasedList', 'detail', 'myProducts'];
+    protected $userProductService;
+
+    public function initialize(): void
+    {
+        parent::initialize();
+        $this->userProductService = new UserProductService();
+    }
+
+    /**
+     * 获取我的产品列表(新版本)
+     */
+    public function myProducts(): void
+    {
+
+            $userId = $this->auth->id;
+            if (!$userId) {
+                $this->error('用户未登录');
+            }
+
+            // 使用新的服务类获取产品列表
+            $products = $this->userProductService->getUserProducts($userId);
+            // 获取用户所在企业信息(用于显示企业套餐管理入口)
+            $companyInfo = null;
+            $companyService = new \app\common\service\CompanyPackageService();
+            $companyData = $companyService->getUserCompany($userId);
+
+            if ($companyData) {
+                $companyInfo = [
+                    'company_id' => $companyData['id'],
+                    'company_name' => $companyData['name'],
+                    'is_creator' => $companyData['creator_id'] == $userId
+                ];
+            }
+        $this->success('', [
+            'products' => $products,
+            'company_info' => $companyInfo,
+            'total' => count($products)
+        ]);
+
+    }
+
+    /**
+     * 获取用户已购买的产品列表(简化版,兼容旧接口)
+     * @deprecated 建议使用myProducts接口
+     */
+    public function purchasedList(): void
+    {
+        // 调用新接口
+        $this->myProducts();
+    }
+
+    /**
+     * 获取产品详情(包含购买记录)
+     */
+    public function detail(): void
+    {
+        $userId = $this->auth->id;
+        $productId = $this->request->get('id', 0);
+
+        if ($productId <= 0) {
+            $this->error('产品ID错误');
+        }
+
+        try {
+            // 1. 获取产品基本信息
+            $product = Db::table('ba_product')
+                ->where('id', $productId)
+                ->where('status', 1)
+                ->find();
+
+            if (!$product) {
+                $this->error('产品不存在');
+            }
+
+            // 2. 获取用户对该产品的使用记录
+            $usageRecords = Db::name('user_product_usage')
+                ->alias('upu')
+                ->leftJoin('ba_order o', 'o.id = upu.order_id')
+                ->leftJoin('ba_package_price pp', 'pp.id = upu.package_price_id')
+                ->where('upu.user_id', $userId)
+                ->where('upu.product_id', $productId)
+                ->where('upu.status', 1) // 有效记录
+                ->field('upu.*, o.order_no, o.pay_time, o.order_status, o.pay_amount,
+                    pp.spec_name, pp.card_type')
+                ->order('upu.create_time', 'desc')
+                ->select()
+                ->toArray();
+
+            // 3. 获取企业套餐的分配记录(如果有)
+            $allocationRecords = [];
+            foreach ($usageRecords as &$record) {
+                if ($record['package_type'] == 2 && $record['allocation_id']) {
+                    $allocation = Db::name('company_package_allocation')
+                        ->alias('cpa')
+                        ->leftJoin('ba_user u', 'u.id = cpa.assigned_by_user_id')
+                        ->where('cpa.id', $record['allocation_id'])
+                        ->field('cpa.*, u.nickname as assigned_by_name')
+                        ->find();
+
+                    if ($allocation) {
+                        $record['allocation_info'] = $allocation;
+                    }
+                }
+
+                // 格式化显示信息
+                $record['display_info'] = $this->getDisplayInfo($record);
+                $record['card_type_text'] = $this->getCardTypeText($record['card_type']);
+                $record['package_type_text'] = $record['package_type'] == 1 ? '个人套餐' : '企业套餐';
+            }
+
+            $this->success('', [
+                'product' => $product,
+                'usage_records' => $usageRecords,
+                'total_records' => count($usageRecords)
+            ]);
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('获取产品详情失败: ' . $e->getMessage());
+            $this->error('获取产品详情失败');
+        }
+    }
+
+    /**
+     * 获取显示信息
+     */
+    private function getDisplayInfo($record)
+    {
+        if ($record['card_type'] >= 3) { // 日期卡
+            return $record['end_time'] ? '到期: ' . date('Y-m-d', strtotime($record['end_time'])) : '永久有效';
+        } elseif ($record['card_type'] == 1) { // 次卡
+            return '剩余次数: ' . $record['remaining_times'];
+        } elseif ($record['card_type'] == 2) { // 积分卡
+            return '剩余积分: ' . $record['remaining_points'];
+        }
+        return '';
+    }
+
+    /**
+     * 获取卡类型文本
+     */
+    private function getCardTypeText($type)
+    {
+        $map = [
+            1 => '次卡',
+            2 => '积分卡',
+            3 => '日卡',
+            4 => '周卡',
+            5 => '季卡',
+            6 => '月卡',
+            7 => '年卡'
+        ];
+        return $map[$type] ?? '其他';
+    }
+}

+ 161 - 0
app/api/controller/invitation/Withdrawal.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace app\api\controller\invitation;
+
+use think\facade\Db;
+use app\common\controller\Frontend;
+use app\common\service\CommissionService;
+
+class Withdrawal extends Frontend
+{
+    protected $commissionService;
+
+    public function initialize(): void
+    {
+        parent::initialize();
+        $this->commissionService = new CommissionService();
+    }
+
+    /**
+     * 申请提现
+     */
+    public function apply(): void
+    {
+        if (!$this->request->isPost()) {
+            $this->error('非法请求');
+        }
+
+        $userId = $this->auth->id;
+        $amount = $this->request->post('amount', 0);
+        $withdrawalMethod = $this->request->post('withdrawal_method', 'wechat');
+        $accountInfo = $this->request->post('account_info', '');
+        $remark = $this->request->post('remark', '');
+
+        if ($amount <= 0) {
+            $this->error('提现金额必须大于0');
+        }
+
+        if (!in_array($withdrawalMethod, ['wechat', 'alipay', 'bank'])) {
+            $this->error('提现方式错误');
+        }
+
+        try {
+            // 检查用户佣金是否足够
+            $commission = Db::name('ba_user_commission')
+                ->where('user_id', $userId)
+                ->find();
+
+            if (!$commission || $commission['current_commission'] < $amount) {
+                $this->error('佣金余额不足');
+            }
+
+            // 生成提现单号
+            $withdrawalNo = 'TX' . date('YmdHis') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
+
+            // 创建提现记录
+            $data = [
+                'withdrawal_no' => $withdrawalNo,
+                'user_id' => $userId,
+                'amount' => $amount,
+                'withdrawal_method' => $withdrawalMethod,
+                'account_info' => $accountInfo,
+                'current_balance' => $commission['current_commission'],
+                'balance_after' => $commission['current_commission'] - $amount,
+                'status' => 0, // 待审核
+                'remark' => $remark,
+                'create_time' => date('Y-m-d H:i:s'),
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            $result = Db::name('ba_withdrawal_record')->insert($data);
+
+            if ($result) {
+                $this->success('提现申请提交成功,等待审核', [
+                    'withdrawal_no' => $withdrawalNo
+                ]);
+            } else {
+                $this->error('提现申请失败');
+            }
+        } catch (\Throwable $e) {
+            $this->error('申请失败:' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取提现记录
+     */
+    public function records(): void
+    {
+        $userId = $this->auth->id;
+        $page = $this->request->get('page', 1);
+        $pageSize = $this->request->get('pageSize', 10);
+
+        try {
+            $list = Db::name('ba_withdrawal_record')
+                ->where('user_id', $userId)
+                ->order('create_time', 'desc')
+                ->paginate([
+                    'page' => $page,
+                    'list_rows' => $pageSize
+                ]);
+
+            $this->success('', [
+                'list' => $list->items(),
+                'total' => $list->total(),
+                'page' => $list->currentPage(),
+                'page_size' => $list->listRows()
+            ]);
+        } catch (\Throwable $e) {
+            $this->error('获取提现记录失败');
+        }
+    }
+
+    /**
+     * 审核提现(管理员用)
+     */
+    public function audit(): void
+    {
+        if (!$this->auth->isLogin() || !$this->auth->isAdmin()) {
+            $this->error('权限不足');
+        }
+
+        $withdrawalId = $this->request->post('withdrawal_id', 0);
+        $status = $this->request->post('status', 0);
+        $auditRemark = $this->request->post('audit_remark', '');
+
+        if ($withdrawalId <= 0) {
+            $this->error('参数错误');
+        }
+
+        if (!in_array($status, [1, 2])) { // 1=通过,2=拒绝
+            $this->error('状态错误');
+        }
+
+        try {
+            if ($status == 1) {
+                // 审核通过,处理提现
+                $result = $this->commissionService->processWithdrawal($withdrawalId);
+            } else {
+                // 审核拒绝
+                $result = Db::name('ba_withdrawal_record')
+                    ->where('id', $withdrawalId)
+                    ->update([
+                        'status' => 2, // 审核拒绝
+                        'audit_time' => date('Y-m-d H:i:s'),
+                        'audit_remark' => $auditRemark,
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+
+                $result = $result !== false;
+            }
+
+            if ($result) {
+                $this->success('审核操作成功');
+            } else {
+                $this->error('审核操作失败');
+            }
+        } catch (\Throwable $e) {
+            $this->error('审核失败:' . $e->getMessage());
+        }
+    }
+}

+ 161 - 0
app/api/controller/product/OrderStatus.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace app\api\controller\product;
+
+use app\common\controller\Frontend;
+use think\facade\Db;
+
+class OrderStatus extends Frontend
+{
+    protected array $noNeedPermission = ['check'];
+    protected array $noNeedLogin = ['check']; // 允许未登录用户查询(通过订单号)
+
+    /**
+     * 查询订单状态(前端轮询调用)
+     * GET /api/product/orderStatus/check?order_no=BUV202601161721166683
+     */
+    public function check()
+    {
+
+            // 1. 获取订单号
+            $orderNo = $this->request->get('order_no', '');
+
+            if (empty($orderNo)) {
+                return json([
+                    'code' => 400,
+                    'msg' => '订单号不能为空',
+                    'data' => null
+                ]);
+            }
+
+            // 2. 查询订单信息
+            $order = Db::name('order')
+                ->where('order_no', $orderNo)
+                ->field('id, order_no, user_id, pay_amount, pay_method, 
+                    pay_status, order_status, pay_time, create_time')
+                ->find();
+
+            if (!$order) {
+                return json([
+                    'code' => 404,
+                    'msg' => '订单不存在',
+                    'data' => null
+                ]);
+            }
+
+            // 3. 查询支付记录(如果有)
+            $paymentRecord = Db::name('payment_record')
+                ->where('order_no', $orderNo)
+                ->field('id, trade_no, status, pay_time as payment_pay_time')
+                ->find();
+
+            // 4. 查询订单商品信息
+            $orderItem = Db::name('order_item')
+                ->where('order_id', $order['id'])
+                ->field('item_name, spec_name, quantity, unit_price')
+                ->find();
+
+            // 5. 格式化返回数据
+            $result = [
+                'order_no' => $order['order_no'],
+                'pay_status' => $order['pay_status'], // 0=待支付, 1=已支付, 2=支付失败
+                'order_status' => $order['order_status'], // 0=待处理, 1=已确认, 2=已发货, 3=已完成, 4=已取消
+                'pay_method' => $order['pay_method'],
+                'pay_amount' => $order['pay_amount'],
+                'pay_time' => $order['pay_time'],
+                'trade_no' => $paymentRecord['trade_no'] ?? '',
+                'product_name' => $orderItem['item_name'] ?? '商品',
+                'product_spec' => $orderItem['spec_name'] ?? '',
+                'quantity' => $orderItem['quantity'] ?? 1,
+                'status_text' => $this->getStatusText($order['pay_status'], $order['order_status'])
+            ];
+
+
+
+        return json([
+            'code' => 1,
+            'msg' => 'success',
+            'data' => $result
+        ]);
+    }
+
+    /**
+     * 获取状态文本
+     */
+    private function getStatusText($payStatus, $orderStatus)
+    {
+        // 支付状态文本
+        $payStatusMap = [
+            0 => '待支付',
+            1 => '已支付',
+            2 => '支付失败'
+        ];
+
+        // 订单状态文本
+        $orderStatusMap = [
+            0 => '待处理',
+            1 => '已确认',
+            2 => '已发货',
+            3 => '已完成',
+            4 => '已取消'
+        ];
+
+        $text = $payStatusMap[$payStatus] ?? '未知支付状态';
+
+        // 如果已支付,显示订单状态
+        if ($payStatus == 1) {
+            $text .= '/' . ($orderStatusMap[$orderStatus] ?? '未知订单状态');
+        }
+
+        return $text;
+    }
+
+    /**
+     * 手动查询微信订单状态(用于调试)
+     */
+    public function queryWechat()
+    {
+        if (!$this->auth->isLogin() || !$this->auth->isAdmin()) {
+            $this->error('权限不足');
+        }
+
+        $orderNo = $this->request->get('order_no', '');
+
+        if (empty($orderNo)) {
+            $this->error('订单号不能为空');
+        }
+
+        try {
+            // 查询支付记录
+            $paymentRecord = Db::name('ba_payment_record')
+                ->where('order_no', $orderNo)
+                ->find();
+
+            if (!$paymentRecord) {
+                $this->error('支付记录不存在');
+            }
+
+            // 查询支付配置
+            $payConfig = Db::name('ba_pay_config')
+                ->where('id', $paymentRecord['pay_config_id'])
+                ->find();
+
+            if (!$payConfig) {
+                $this->error('支付配置不存在');
+            }
+
+            // 调用微信查询接口
+            $paymentService = \app\common\service\PaymentFactory::create(
+                $paymentRecord['pay_method'],
+                $payConfig['id']
+            );
+
+            $result = $paymentService->queryOrder($orderNo);
+
+            $this->success('查询成功', $result);
+
+        } catch (\Exception $e) {
+            $this->error('查询失败: ' . $e->getMessage());
+        }
+    }
+}

+ 309 - 0
app/api/controller/product/Package.php

@@ -0,0 +1,309 @@
+<?php
+
+namespace app\api\controller\product;
+
+use app\common\controller\Frontend;
+use think\facade\Db;
+
+class Package extends Frontend
+{
+    protected array $noNeedPermission = ['product_list','list', 'detail'];
+    protected array $noNeedLogin = ['*'];
+
+    /**
+     * 获取产品列表(简化版)
+     */
+    public function product_list(): void
+    {
+
+            // 查询所有上架的产品
+            $products = Db::table('ba_product')
+                ->where('status', 1)
+                ->where('stock_status', 1) // 库存充足
+                ->order('sort', 'desc')
+                ->order('id', 'desc')
+                ->field('id, name, image, description, package_id')
+                ->select()
+                ->toArray();
+
+//            // 查询每个产品的套餐信息
+//            foreach ($products as &$product) {
+//                // 获取产品对应的所有套餐规格
+//                $product['packages'] = $this->getProductPackages($product['id'], $product['package_id']);
+//
+//                // 获取主图
+//                $product['main_image'] = $this->getMainImage($product['image']);
+//            }
+
+            $this->success('', $products);
+
+    }
+
+    /**
+     * 获取产品对应的套餐
+     */
+    private function getProductPackages($productId, $packageId, $detail = false)
+    {
+        // 查询套餐规格
+        $query = Db::table('ba_package_price')
+            ->alias('pp')
+            ->leftJoin('ba_package p', 'p.id = pp.package_id')
+            ->where('p.status', 1)
+            ->where('pp.price', '>', 0);
+
+        // 如果产品有关联的package_id,使用它
+        if ($packageId) {
+            $query->where('pp.package_id', $packageId);
+        } else {
+            // 如果没有,可能需要其他关联逻辑
+            // 这里简化处理,返回空
+            return [];
+        }
+
+        // 如果是详情页,获取更多字段
+        if ($detail) {
+            $packages = $query->field('
+                pp.id,
+                pp.spec_name,
+                pp.price,
+                pp.type,
+                pp.card_type,
+                pp.unit_value,
+                pp.points_value,
+                pp.stock,
+                pp.sold_count,
+                pp.description,
+                p.name as package_name
+            ')->select()->toArray();
+        } else {
+            // 列表页只需要基本信息
+            $packages = $query->field('
+                pp.id,
+                pp.spec_name,
+                pp.price,
+                pp.type,
+                pp.card_type,
+                pp.unit_value,
+                pp.points_value,
+                pp.stock,
+                pp.sold_count
+            ')->select()->toArray();
+        }
+
+        // 格式化数据
+        $formattedPackages = [];
+        foreach ($packages as $package) {
+            $formattedPackage = [
+                'id' => $package['id'],
+                'name' => $package['spec_name'],
+                'type' => $package['type'],
+                'type_text' => $package['type'] == 1 ? '个人' : '企业',
+                'price' => floatval($package['price']),
+                'price_text' => '¥' . number_format($package['price'], 2),
+                'stock' => intval($package['stock']),
+                'stock_text' => $this->getStockText($package['stock']),
+                'sold_count' => intval($package['sold_count']),
+                'card_type' => $package['card_type'],
+                'card_info' => $this->getCardInfo($package['card_type'], $package['unit_value'], $package['points_value'])
+            ];
+
+            // 如果是详情页,添加更多信息
+            if ($detail) {
+                $formattedPackage['description'] = $package['description'] ?? '商品说明';
+                $formattedPackage['package_name'] = $package['package_name'] ?? '';
+            }
+
+            $formattedPackages[] = $formattedPackage;
+        }
+
+        return $formattedPackages;
+    }
+
+    /**
+     * 获取套餐列表
+     */
+    public function list(): void
+    {
+
+            // 获取参数(可根据产品筛选)
+            $productId = $this->request->get('product_id', 0);
+
+            // 构建查询
+            $query = Db::table('ba_package_price')
+                ->alias('pp')
+                ->leftJoin('ba_package p', 'p.id = pp.package_id')
+                ->where('p.status', 1)
+                ->where('pp.price', '>', 0)
+                ->order('pp.type', 'asc') // 个人在前,企业在后
+                ->order('pp.price', 'asc'); // 价格从低到高
+
+            // 如果有产品ID,筛选对应的套餐
+            if ($productId > 0) {
+                $product = Db::table('ba_product')
+                    ->where('id', $productId)
+                    ->field('package_id')
+                    ->find();
+                if ($product && $product['package_id']) {
+                    $query->where('pp.package_id', $product['package_id']);
+                }
+            }
+
+            // 获取套餐数据
+            $packages = $query->field('
+                pp.id,
+                pp.spec_name,
+                pp.image,
+                pp.price,
+                pp.type,
+                pp.card_type,
+                pp.unit_value,
+                pp.points_value,
+                pp.stock,
+                pp.sold_count,
+                pp.description
+            ')->select()->toArray();
+            // 格式化返回数据(严格按照截图格式)
+            $formattedPackages = [];
+            foreach ($packages as $package) {
+                $formattedPackage = [
+                    'id' => $package['id'],
+                    'name' => $package['spec_name'], // 套餐名称
+                    'type' => $package['type'], // 1个人,2企业
+                    'type_text' => $package['type'] == 1 ? '个人' : '企业',
+                    'price' => floatval($package['price']),
+                    'price_text' => '¥' . number_format($package['price'], 2),
+                    'description' => $package['description'] ?? '商品说明',
+                    'stock' => intval($package['stock']),
+                    'stock_text' => $this->getStockText($package['stock']),
+                    'sold_count' => intval($package['sold_count']),
+                    'card_type' => $package['card_type'],
+                    'image' => $package['image'],
+                    'card_info' => $this->getCardInfo($package['card_type'], $package['unit_value'], $package['points_value'])
+                ];
+
+                $formattedPackages[] = $formattedPackage;
+            }
+
+            $this->success('', $formattedPackages);
+
+    }
+
+    /**
+     * 获取套餐详情(用于购买页面)
+     */
+    public function detail(): void
+    {
+            $packageId = $this->request->get('id', 0);
+
+            if ($packageId <= 0) {
+                $this->error('套餐ID错误');
+            }
+
+            // 查询套餐详情
+            $package = Db::table('ba_package_price')
+                ->alias('pp')
+                ->leftJoin('ba_package p', 'p.id = pp.package_id')
+                ->where('pp.id', $packageId)
+                ->field('
+                    pp.id,
+                    pp.image,
+                    pp.spec_name,
+                    pp.price,
+                    pp.type,
+                    pp.card_type,
+                    pp.unit_value,
+                    pp.points_value,
+                    pp.stock,
+                    pp.sold_count,
+                    pp.description,
+                    p.name as package_name,
+                    p.description as package_description
+                ')
+                ->find();
+
+            if (!$package) {
+                $this->error('套餐不存在');
+            }
+
+            // 查询关联的产品信息
+            $product = Db::table('ba_product')
+                ->where('package_id', $package['package_id'] ?? 0)
+                ->field('id, name, image')
+                ->find();
+
+            // 格式化详情数据
+            $formattedDetail = [
+                'id' => $package['id'],
+                'name' => $package['spec_name'],
+                'type' => $package['type'],
+                'type_text' => $package['type'] == 1 ? '个人' : '企业',
+                'price' => floatval($package['price']),
+                'price_text' => '¥' . number_format($package['price'], 2),
+                'description' => $package['description'] ?? ($package['package_description'] ?? '商品说明'),
+                'stock' => intval($package['stock']),
+                'stock_text' => $this->getStockText($package['stock']),
+                'sold_count' => intval($package['sold_count']),
+                'card_type' => $package['card_type'],
+                'card_info' => $this->getCardInfo($package['card_type'], $package['unit_value'], $package['points_value']),
+                'package_name' => $package['package_name'],
+                'image' => $package['image'],
+                'product' => $product ? [
+                    'id' => $product['id'],
+                    'name' => $product['name'],
+                    'image' => $this->getMainImage($product['image'])
+                ] : null
+            ];
+
+            $this->success('', $formattedDetail);
+
+    }
+
+    /**
+     * 获取库存状态文本
+     */
+    private function getStockText($stock): string
+    {
+        if ($stock <= 0) {
+            return '售罄';
+        } elseif ($stock <= 10) {
+            return '即将售罄';
+        } else {
+            return '充足';
+        }
+    }
+
+    /**
+     * 获取卡类型信息
+     */
+    private function getCardInfo($cardType, $unitValue, $pointsValue): array
+    {
+        $cardTypes = [
+            1 => ['name' => '次卡', 'show_value' => $unitValue . '次'],
+            2 => ['name' => '积分卡', 'show_value' => $pointsValue . '积分'],
+            3 => ['name' => '日卡', 'show_value' => '1天'],
+            4 => ['name' => '周卡', 'show_value' => '7天'],
+            5 => ['name' => '季卡', 'show_value' => '90天'],
+            6 => ['name' => '月卡', 'show_value' => '30天'],
+            7 => ['name' => '年卡', 'show_value' => '365天']
+        ];
+
+        return $cardTypes[$cardType] ?? ['name' => '其他', 'show_value' => ''];
+    }
+
+    /**
+     * 获取主图
+     */
+    private function getMainImage($imageJson)
+    {
+        if (empty($imageJson)) {
+            return '';
+        }
+
+        $images = json_decode($imageJson, true);
+        if (is_array($images) && !empty($images)) {
+            return $images[0];
+        }
+
+        return '';
+    }
+}

+ 744 - 0
app/api/controller/product/Payment.php

@@ -0,0 +1,744 @@
+<?php
+
+namespace app\api\controller\product;
+
+use app\common\controller\Frontend;
+use app\common\service\PaymentFactory;
+use think\facade\Db;
+use think\facade\Log;
+
+class Payment extends Frontend
+{
+    protected array $noNeedPermission = ['createQrCode', 'queryStatus', 'wechatNotify', 'alipayNotify', 'paymentSuccess'];
+    protected array $noNeedLogin = ['*'];
+    /**
+     * 创建支付二维码
+     */
+    public function createQrCode()
+    {
+        $userId = $this->auth->id;
+        $orderId = $this->request->post('order_id', 0);
+        $payMethod = $this->request->post('pay_method', 'wechat');
+        if (!$userId || $orderId <= 0) {
+            $this->error('参数错误');
+        }
+
+        // 验证支付方式
+        if (!in_array($payMethod, ['wechat', 'alipay'])) {
+            $this->error('不支持的支付方式');
+        }
+
+        // 查询订单信息
+        $order = Db::table('ba_order')
+            ->where('id', $orderId)
+            ->where('user_id', $userId)
+            ->where('pay_status', 0) // 待支付
+            ->find();
+        if (!$order) {
+            $this->error('订单不存在或已支付');
+        }
+
+        // 更新订单支付方式
+        Db::table('ba_order')
+            ->where('id', $orderId)
+            ->update([
+                'pay_method' => $payMethod,
+                'update_time' => date('Y-m-d H:i:s')
+            ]);
+
+        // 检查是否已有支付记录
+        $paymentRecord = Db::table('ba_payment_record')
+            ->where('order_id', $orderId)
+            ->where('status', 0) // 待支付
+            ->find();
+        if ($paymentRecord) {
+            // 检查支付记录是否过期(15分钟)
+            $createTime = strtotime($paymentRecord['create_time']);
+            if (time() - $createTime < 900) { // 15分钟
+                $this->success('', [
+                    'order_no' => $order['order_no'],
+                    'pay_amount' => $order['pay_amount'],
+                    'qr_code_url' => $paymentRecord['qr_code_url'],
+                    'qr_code_content' => $paymentRecord['qr_code_content'],
+                    'payment_id' => $paymentRecord['id'],
+                ]);
+            }
+        }
+
+        // 查询订单商品信息
+        $orderItem = Db::table('ba_order_item')
+            ->where('order_id', $orderId)
+            ->find();
+
+        $subject = $orderItem['item_name'] ?? '商品购买';
+        $body = $orderItem['spec_name'] ?? '';
+
+        // 创建支付服务实例
+        $paymentService = PaymentFactory::create($payMethod);
+        // 生成支付二维码
+        $qrCodeResult = $paymentService->createQrCode(
+            $order['order_no'],
+            $order['pay_amount'],
+            $subject,
+            $body
+        );
+        // 创建支付记录
+        $payConfig = $paymentService->getPayConfig($payMethod); // 需要添加这个方法到BasePaymentService
+
+        $paymentData = [
+            'order_id' => $orderId,
+            'order_no' => $order['order_no'],
+            'pay_config_id' => $payConfig['id'],
+            'pay_method' => $payMethod,
+            'pay_amount' => $order['pay_amount'],
+            'qr_code_url' => $qrCodeResult['qr_code_url'],
+            'qr_code_content' => $qrCodeResult['qr_code_content'],
+            'status' => 0,
+            'create_time' => date('Y-m-d H:i:s'),
+            'update_time' => date('Y-m-d H:i:s'),
+        ];
+
+        if ($paymentRecord) {
+            // 更新现有记录
+            Db::table('ba_payment_record')
+                ->where('id', $paymentRecord['id'])
+                ->update($paymentData);
+            $paymentId = $paymentRecord['id'];
+        } else {
+            // 创建新记录
+            $paymentId = Db::table('ba_payment_record')->insertGetId($paymentData);
+        }
+
+        $this->success('创建成功', [
+            'order_no' => $order['order_no'],
+            'pay_amount' => $order['pay_amount'],
+            'qr_code_url' => $qrCodeResult['qr_code_url'],
+            'qr_code_content' => $qrCodeResult['qr_code_content'],
+            'payment_id' => $paymentId,
+            'expire_time' => 900, // 15分钟过期
+        ]);
+    }
+
+    /**
+     * 查询支付状态
+     */
+    public function queryStatus()
+    {
+        try {
+            $orderNo = $this->request->get('order_no', '');
+
+            if (empty($orderNo)) {
+                $this->error('订单号错误');
+            }
+
+            // 查询支付记录
+            $paymentRecord = Db::table('ba_payment_record')
+                ->where('order_no', $orderNo)
+                ->find();
+
+            if (!$paymentRecord) {
+                $this->error('支付记录不存在');
+            }
+
+            // 如果已支付,直接返回
+            if ($paymentRecord['status'] == 1) {
+                $this->success('', [
+                    'pay_status' => 1,
+                    'pay_time' => $paymentRecord['pay_time'],
+                    'trade_no' => $paymentRecord['trade_no'],
+                    'message' => '支付成功',
+                ]);
+            }
+
+            // 检查是否过期
+            $createTime = strtotime($paymentRecord['create_time']);
+            if (time() - $createTime > 900) { // 15分钟
+                // 更新为已关闭
+                Db::table('ba_payment_record')
+                    ->where('id', $paymentRecord['id'])
+                    ->update([
+                        'status' => 3,
+                        'update_time' => date('Y-m-d H:i:s'),
+                    ]);
+
+                $this->success('', [
+                    'pay_status' => 3,
+                    'message' => '支付已超时',
+                ]);
+            }
+
+            // 查询支付配置
+            $payConfig = Db::table('ba_pay_config')
+                ->where('id', $paymentRecord['pay_config_id'])
+                ->find();
+
+            if (!$payConfig) {
+                $this->error('支付配置不存在');
+            }
+
+            // 查询支付平台
+            $paymentService = PaymentFactory::create($paymentRecord['pay_method'], $payConfig['id']);
+            $queryResult = $paymentService->queryOrder($orderNo);
+
+            // 如果支付成功,处理支付成功逻辑
+            if ($queryResult['pay_status'] == 1) {
+                $this->processPaymentSuccess(
+                    $orderNo,
+                    $queryResult['trade_no'],
+                    $queryResult['pay_amount'],
+                    $queryResult['pay_time']
+                );
+            }
+
+            $this->success('', [
+                'pay_status' => $queryResult['pay_status'],
+                'pay_time' => $queryResult['pay_time'] ?? null,
+                'trade_no' => $queryResult['trade_no'] ?? null,
+                'message' => $this->getStatusMessage($queryResult['pay_status']),
+            ]);
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('查询支付状态失败: ' . $e->getMessage());
+            $this->error('查询支付状态失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 微信支付回调
+     */
+    public function wechatNotify()
+    {
+        try {
+            // 获取原始输入
+            $input = file_get_contents('php://input');
+
+            if (empty($input)) {
+                throw new \Exception('回调数据为空');
+            }
+
+            Log::info('微信支付回调数据: ' . $input);
+
+            // 解析XML
+            $data = $this->xmlToArray($input);
+            $orderNo = $data['out_trade_no'] ?? '';
+
+            if (empty($orderNo)) {
+                throw new \Exception('未找到订单号');
+            }
+
+            // 使用事务和锁处理回调
+            Db::startTrans();
+            try {
+                // 使用行锁检查支付记录
+                $paymentRecord = Db::table('ba_payment_record')
+                    ->where('order_no', $orderNo)
+                    ->lock(true)
+                    ->find();
+
+                if (!$paymentRecord) {
+                    throw new \Exception('支付记录不存在: ' . $orderNo);
+                }
+
+                // 如果已处理,直接返回成功响应(幂等性)
+                if ($paymentRecord['status'] == 1) {
+                    Db::rollback();
+                    Log::info('微信支付回调幂等性检查:订单已处理,直接返回成功', [
+                        'order_no' => $orderNo,
+                        'payment_record_id' => $paymentRecord['id']
+                    ]);
+
+                    $response = [
+                        'return_code' => 'SUCCESS',
+                        'return_msg' => 'OK',
+                    ];
+                    echo $this->arrayToXml($response);
+                    return;
+                }
+
+                // 查询支付配置
+                $payConfig = Db::table('ba_pay_config')
+                    ->where('id', $paymentRecord['pay_config_id'])
+                    ->find();
+
+                if (!$payConfig) {
+                    throw new \Exception('支付配置不存在');
+                }
+
+                // 处理回调
+                $paymentService = PaymentFactory::create('wechat', $payConfig['id']);
+                $result = $paymentService->handleNotify($input);
+
+                // 处理支付成功
+                $this->processPaymentSuccess(
+                    $result['order_no'],
+                    $result['trade_no'],
+                    $result['pay_amount'],
+                    $result['pay_time']
+                );
+
+                Db::commit();
+
+                // 返回成功响应给微信
+                $response = [
+                    'return_code' => 'SUCCESS',
+                    'return_msg' => 'OK',
+                ];
+
+                echo $this->arrayToXml($response);
+
+            } catch (\Throwable $e) {
+                Db::rollback();
+                throw $e;
+            }
+
+        } catch (\Throwable $e) {
+            Log::error('微信支付回调处理失败: ' . $e->getMessage());
+
+            $response = [
+                'return_code' => 'FAIL',
+                'return_msg' => $e->getMessage(),
+            ];
+            echo $this->arrayToXml($response);
+        }
+    }
+
+    /**
+     * 支付宝支付回调
+     */
+    public function alipayNotify()
+    {
+        try {
+            $input = $_POST;
+
+            if (empty($input)) {
+                throw new \Exception('回调数据为空');
+            }
+
+            // 记录回调日志
+            \think\facade\Log::info('支付宝支付回调数据: ' . json_encode($input));
+
+            $orderNo = $input['out_trade_no'] ?? '';
+
+            if (empty($orderNo)) {
+                throw new \Exception('未找到订单号');
+            }
+
+            // 幂等性检查:检查订单是否已处理
+            $processedOrder = Db::table('ba_payment_record')
+                ->where('order_no', $orderNo)
+                ->where('status', 1) // 已支付
+                ->find();
+
+            if ($processedOrder) {
+                // 如果已处理,直接返回成功响应
+                \think\facade\Log::info('支付宝支付回调幂等性检查:订单已处理,直接返回成功', [
+                    'order_no' => $orderNo,
+                    'payment_record_id' => $processedOrder['id']
+                ]);
+
+                echo 'success';
+                return;
+            }
+
+            // 查询支付记录
+            $paymentRecord = Db::table('ba_payment_record')
+                ->where('order_no', $orderNo)
+                ->find();
+
+            if (!$paymentRecord) {
+                throw new \Exception('支付记录不存在: ' . $orderNo);
+            }
+
+            // 查询支付配置
+            $payConfig = Db::table('ba_pay_config')
+                ->where('id', $paymentRecord['pay_config_id'])
+                ->find();
+
+            if (!$payConfig) {
+                throw new \Exception('支付配置不存在');
+            }
+
+            // 处理回调
+            $paymentService = PaymentFactory::create('alipay', $payConfig['id']);
+            $result = $paymentService->handleNotify($input);
+
+            // 处理支付成功
+            $this->processPaymentSuccess(
+                $result['order_no'],
+                $result['trade_no'],
+                $result['pay_amount'],
+                $result['pay_time']
+            );
+
+            echo 'success';
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('支付宝支付回调处理失败: ' . $e->getMessage());
+            echo 'fail';
+        }
+    }
+
+    /**
+     * 处理支付成功
+     */
+    private function processPaymentSuccess($orderNo, $tradeNo, $payAmount, $payTime)
+    {
+        // 使用数据库事务和锁来确保幂等性
+        Db::startTrans();
+
+        try {
+            // 1. 使用行锁检查订单是否已处理
+            $order = Db::table('ba_order')
+                ->where('order_no', $orderNo)
+                ->lock(true) // 行锁
+                ->find();
+
+            if (!$order) {
+                Db::rollback();
+                Log::error('订单不存在: ' . $orderNo);
+                throw new \Exception('订单不存在');
+            }
+
+            // 2. 如果订单已支付,直接返回(幂等性)
+            if ($order['pay_status'] == 1) {
+                Db::rollback();
+                Log::info('订单已支付,跳过处理(幂等性)', ['order_no' => $orderNo]);
+                return;
+            }
+
+            // 3. 更新支付记录
+            Db::table('ba_payment_record')
+                ->where('order_no', $orderNo)
+                ->where('status', 0) // 只更新待支付的记录
+                ->update([
+                    'status' => 1,
+                    'trade_no' => $tradeNo,
+                    'pay_time' => $payTime,
+                    'notify_time' => date('Y-m-d H:i:s'),
+                    'update_time' => date('Y-m-d H:i:s'),
+                ]);
+
+            // 4. 更新订单状态
+            Db::table('ba_order')
+                ->where('order_no', $orderNo)
+                ->where('pay_status', 0) // 只更新未支付的订单
+                ->update([
+                    'pay_status' => 1,
+                    'pay_time' => $payTime,
+                    'order_status' => 1,
+                    'update_time' => date('Y-m-d H:i:s'),
+                ]);
+
+            // 5. 获取更新后的订单
+            $order = Db::table('ba_order')
+                ->where('order_no', $orderNo)
+                ->find();
+
+            // 6. 获取订单商品
+            $orderItems = Db::table('ba_order_item')
+                ->where('order_id', $order['id'])
+                ->select();
+
+            // 7. 创建用户产品服务实例
+            $userProductService = new \app\common\service\UserProductService();
+            $companyPackageService = new \app\common\service\CompanyPackageService();
+
+            foreach ($orderItems as $item) {
+                // 8. 使用事务和锁检查是否已存在使用记录
+                $existingUsage = Db::table('ba_user_product_usage')
+                    ->where('order_item_id', $item['id'])
+                    ->where('user_id', $order['user_id'])
+                    ->lock(true)
+                    ->find();
+
+                if ($existingUsage) {
+                    Log::info('订单商品已处理,跳过', [
+                        'order_item_id' => $item['id'],
+                        'order_id' => $order['id']
+                    ]);
+                    continue;
+                }
+
+                // 9. 获取套餐信息
+                $package = Db::table('ba_package_price')
+                    ->where('id', $item['package_price_id'])
+                    ->find();
+
+                if (!$package) {
+                    throw new \Exception('套餐不存在: ' . $item['package_price_id']);
+                }
+
+                Log::info('处理订单商品', [
+                    'order_item_id' => $item['id'],
+                    'product_id' => $item['product_id'],
+                    'package_id' => $item['package_price_id'],
+                    'package_type' => $package['type'],
+                    'card_type' => $package['card_type'],
+                    'quantity' => $item['quantity']
+                ]);
+
+                if ($package['type'] == 1) {
+                    // 个人套餐:立即创建使用记录
+                    $userProductService->createUserProductRecord($order['user_id'], $item, $package);
+                } else {
+                    // 企业套餐:创建席位记录
+                    // 首先验证企业购买
+                    $companyInfo = $companyPackageService->getUserCompany($order['user_id']);
+                    if (!$companyInfo) {
+                        throw new \Exception('企业套餐购买失败:用户不属于任何企业');
+                    }
+
+                    // 创建企业套餐席位记录
+                    $companyPackageService->processCompanyPackagePurchase(
+                        $order['id'],
+                        $companyInfo['id'],
+                        $package['id'],
+                        $item['quantity'],
+                        $package['card_type'],
+                        $package['unit_value'],
+                        $package['points_value']
+                    );
+
+                    // 企业套餐购买后,自动分配一个席位给购买者
+                    $companyPackageService->autoAllocateToBuyer(
+                        $order['user_id'],
+                        $companyInfo['id'],
+                        $package['id'],
+                        $order['id']
+                    );
+
+                    // 注意:这里不再重复创建用户产品记录!
+                    // 因为 autoAllocateToBuyer 中已经会创建
+                    // 只有次卡/积分卡需要在购买时也创建使用记录
+                    if ($package['card_type'] == 1 || $package['card_type'] == 2) {
+                        // 使用事务安全的方式创建
+                        $this->createUserProductRecordSafe($order['user_id'], $item, $package);
+                    }
+                }
+
+                // 10. 扣减库存
+                $this->deductStockAfterPayment($order['id']);
+            }
+
+            // 11. 处理分销佣金
+            if ($order['is_commission'] == 0 && $order['distributor_id'] && $order['commission_amount'] > 0) {
+                $this->processCommission($order);
+            }
+
+            Db::commit();
+            Log::info('支付成功处理完成', ['order_no' => $orderNo]);
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('处理支付成功失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * 安全创建用户产品记录(防止重复)
+     */
+    private function createUserProductRecordSafe($userId, $orderItem, $package)
+    {
+        try {
+            // 检查是否已存在
+            $existing = Db::table('ba_user_product_usage')
+                ->where('order_item_id', $orderItem['id'])
+                ->where('user_id', $userId)
+                ->find();
+
+            if ($existing) {
+                Log::info('用户产品记录已存在,跳过创建', [
+                    'order_item_id' => $orderItem['id'],
+                    'user_id' => $userId
+                ]);
+                return $existing['id'];
+            }
+
+            // 创建用户产品服务实例
+            $userProductService = new \app\common\service\UserProductService();
+            return $userProductService->createUserProductRecord($userId, $orderItem, $package);
+
+        } catch (\Throwable $e) {
+            Log::error('创建用户产品记录失败: ' . $e->getMessage());
+            // 不抛出异常,避免影响主流程
+            return false;
+        }
+    }
+
+    /**
+     * 支付成功后扣减库存
+     */
+    private function deductStockAfterPayment($orderId)
+    {
+        try {
+            // 查询订单商品
+            $orderItem = Db::table('ba_order_item')
+                ->where('order_id', $orderId)
+                ->find();
+
+            if (!$orderItem) {
+                return;
+            }
+
+            // 扣减套餐库存
+            if ($orderItem['package_price_id']) {
+                $result = Db::table('ba_package_price')
+                    ->where('id', $orderItem['package_price_id'])
+                    ->where('stock', '>=', $orderItem['quantity']) // 再次检查库存
+                    ->dec('stock', $orderItem['quantity'])
+                    ->inc('sold_count', $orderItem['quantity'])
+                    ->update();
+
+                if (!$result) {
+                    // 库存不足,需要处理异常(退款或补货)
+                    \think\facade\Log::error('支付成功后库存不足,订单ID:' . $orderId);
+                    // 这里可以触发退款逻辑或人工处理
+                }
+            }
+
+            // 如果有产品库存,也可以在这里扣减
+            if ($orderItem['product_id']) {
+                // 如果产品表有库存字段,在这里扣减
+                // Db::table('ba_product')...
+            }
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('扣减库存失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 处理分销佣金
+     */
+    private function processCommission($order)
+    {
+        try {
+            // 调用佣金服务
+            $commissionService = new \app\common\service\CommissionService();
+
+            $result = $commissionService->updateDistributionRecord($order['id'], 2, $order['pay_time']);
+
+            if ($result) {
+                // 2. 更新分销人的佣金统计
+                $commissionService->updateUserCommission($order['distributor_id']);
+
+                // 3. 标记订单佣金已处理
+                Db::table('ba_order')
+                    ->where('id', $order['id'])
+                    ->update([
+                        'is_commission' => 1,
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            }
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('处理分销佣金失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 确认订单库存
+     */
+    private function confirmOrderStock($orderId)
+    {
+        try {
+            // 查询订单商品
+            $orderItems = Db::table('ba_order_item')
+                ->where('order_id', $orderId)
+                ->select();
+
+            foreach ($orderItems as $item) {
+                if ($item['package_price_id']) {
+                    // 可以在这里记录库存确认,或者更新相关状态
+                    // 由于创建订单时已经扣减库存,这里主要做确认处理
+                }
+            }
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('确认订单库存失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 发送支付成功通知
+     */
+    private function sendPaymentSuccessNotify($order)
+    {
+        // 这里可以实现发送微信模板消息、短信、站内信等通知
+        // 根据业务需求实现
+
+        \think\facade\Log::info('支付成功通知: 订单号=' . $order['order_no'] . ', 用户ID=' . $order['user_id']);
+    }
+
+    /**
+     * 获取状态消息
+     */
+    private function getStatusMessage($status)
+    {
+        $messages = [
+            0 => '等待支付',
+            1 => '支付成功',
+            2 => '支付失败',
+            3 => '支付已关闭',
+        ];
+
+        return $messages[$status] ?? '未知状态';
+    }
+
+    /**
+     * XML转数组
+     */
+    private function xmlToArray($xml)
+    {
+        if (empty($xml)) {
+            return [];
+        }
+
+        try {
+            // 使用DOMDocument方式,兼容PHP 8.0+
+            $dom = new \DOMDocument();
+
+            if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
+                // PHP 8.0+ 直接加载
+                $dom->loadXML($xml, LIBXML_NOENT | LIBXML_NOCDATA);
+            } else {
+                // PHP 8.0以下需要禁用实体加载器
+                $previous = libxml_disable_entity_loader(true);
+                $dom->loadXML($xml, LIBXML_NOENT | LIBXML_NOCDATA);
+                libxml_disable_entity_loader($previous);
+            }
+
+            // 简单的转换方法
+            $data = [];
+            foreach ($dom->documentElement->childNodes as $node) {
+                if ($node->nodeType == XML_ELEMENT_NODE) {
+                    $data[$node->nodeName] = $node->nodeValue;
+                }
+            }
+
+            return $data;
+
+        } catch (\Exception $e) {
+            \think\facade\Log::error('XML解析失败: ' . $e->getMessage());
+            return [];
+        }
+    }
+
+    /**
+     * 数组转XML
+     */
+    private function arrayToXml($arr)
+    {
+        $xml = '<xml>';
+        foreach ($arr as $key => $val) {
+            if (is_numeric($val)) {
+                $xml .= "<{$key}>{$val}</{$key}>";
+            } else {
+                $xml .= "<{$key}><![CDATA[{$val}]]></{$key}>";
+            }
+        }
+        $xml .= '</xml>';
+        return $xml;
+    }
+}

+ 290 - 0
app/api/controller/product/Purchase.php

@@ -0,0 +1,290 @@
+<?php
+
+namespace app\api\controller\product;
+
+use app\common\controller\Frontend;
+use app\common\service\CompanyPackageService;
+use think\facade\Db;
+
+class Purchase extends Frontend
+{
+    protected array $noNeedPermission = ['createOrder', 'orderDetail'];
+
+    /**
+     * 创建购买订单
+     */
+    public function createOrder(): void
+    {
+
+            if (!$this->request->isPost()) {
+                $this->error('非法请求');
+            }
+
+            $userId = $this->auth->id;
+            if (!$userId) {
+                $this->error('请先登录');
+            }
+            // 获取参数
+            $packageId = $this->request->post('package_id', 0);
+            $quantity = $this->request->post('quantity', 1);
+            $distributorId = $this->request->post('distributor_id', 0); // 分销人ID
+
+            if ($packageId <= 0) {
+                $this->error('请选择套餐');
+            }
+
+            if ($quantity < 1) {
+                $this->error('购买数量错误');
+            }
+
+            // 查询套餐详情
+            $package = Db::table('ba_package_price')
+                ->alias('pp')
+                ->leftJoin('ba_package p', 'p.id = pp.package_id')
+                ->where('pp.id', $packageId)
+                ->field('
+                    pp.*,
+                    p.name as package_name
+                ')
+                ->find();
+
+            if (!$package) {
+                $this->error('套餐不存在');
+            }
+
+            // 检查库存
+            if ($package['stock'] < $quantity) {
+                $this->error('库存不足');
+            }
+
+        // 在原有验证后,添加企业套餐验证
+        if ($package['type'] == 2) { // 企业套餐
+            $companyService = new CompanyPackageService();
+            $validationResult = $companyService->validateCompanyPurchase($userId, $packageId, $quantity);
+
+            if (!$validationResult['valid']) {
+                $this->error('企业套餐购买验证失败');
+            }
+
+            $companyId = $validationResult['company_id'];
+        }
+
+            // 查找关联的产品
+            $product = Db::table('ba_product')
+                ->where('package_id', $package['package_id'])
+                ->field('id, name')
+                ->find();
+            if (!$product) {
+                $this->error('关联产品不存在');
+            }
+
+            // 计算金额
+            $totalAmount = $package['price'] * $quantity;
+
+        // 计算分销佣金
+        $commissionService = new \app\common\service\CommissionService();
+        $commissionResult = $commissionService->calculateCommission($totalAmount, $distributorId);
+        $commissionAmount = $commissionResult['commission_amount'];
+        $commissionRate = $commissionResult['commission_rate'];
+        $hasCommission = $commissionResult['has_commission'];
+
+            // 生成订单号
+            $orderNo = 'BUY' . date('YmdHis') . mt_rand(1000, 9999);
+            Db::startTrans();
+
+            try {
+                // 1. 创建订单
+                $orderData = [
+                    'order_no' => $orderNo,
+                    'user_id' => $userId,
+                    'company_id' => $companyId,
+                    'total_amount' => $totalAmount,
+                    'discount_amount' => 0.00,
+                    'pay_amount' => $totalAmount,
+                    'pay_method' => null, // 支付方式待选择
+                    'pay_status' => 0, // 0=待支付
+                    'order_status' => 0, // 0=待处理
+                    'is_commission' => $hasCommission ? 0 : 0, // 0=未处理佣金,1=已处理
+                    'distributor_id' => $hasCommission ? $distributorId : null,
+                    'commission_amount' => $commissionAmount,
+                    'order_time' => date('Y-m-d H:i:s'),
+                    'create_time' => date('Y-m-d H:i:s'),
+                    'update_time' => date('Y-m-d H:i:s')
+                ];
+                $orderId = Db::table('ba_order')->insertGetId($orderData);
+
+                // 2. 创建订单商品
+                $itemData = [
+                    'order_id' => $orderId,
+                    'product_id' => $product['id'],
+                    'package_id' => $package['package_id'],
+                    'package_price_id' => $packageId,
+                    'company_id' => $companyId,
+                    'item_type' => 2, // 套餐
+                    'item_name' => $product['name'] . ' - ' . $package['spec_name'],
+                    'spec_name' => $package['spec_name'],
+                    'quantity' => $quantity,
+                    'unit_price' => $package['price'],
+                    'total_price' => $totalAmount,
+                    'create_time' => date('Y-m-d H:i:s')
+                ];
+
+                Db::table('ba_order_item')->insert($itemData);
+
+                // 3. 如果存在分销,创建分销记录
+                if ($hasCommission && $distributorId > 0) {
+                    $commissionService->createDistributionRecord([
+                        'order_id' => $orderId,
+                        'product_id' => $product['id'],
+                        'buyer_id' => $userId,
+                        'distributor_id' => $distributorId,
+                        'commission_rate' => $commissionRate,
+                        'commission_amount' => $commissionAmount
+                    ]);
+                }
+                // 如果是企业套餐,处理企业套餐相关逻辑
+//                if ($package['type'] == 2) {
+//                    // 在企业购买配额表中记录
+//                    $companyService->processCompanyPackagePurchase(
+//                        $orderId,
+//                        $companyId,
+//                        $packageId,
+//                        $quantity,
+//                        $package['card_type'],
+//                        $package['unit_value'],
+//                        $package['points_value']
+//                    );
+//
+//                    $orderData['company_id'] = $companyId;
+//                }
+
+                Db::commit();
+            } catch (\Throwable $e) {
+                Db::rollback();
+                throw $e;
+            }
+
+        $this->success('订单创建成功', [
+            'order_id' => $orderId,
+            'order_no' => $orderNo,
+            'product_name' => $product['name'],
+            'package_name' => $package['spec_name'],
+            'total_amount' => $totalAmount,
+            'quantity' => $quantity,
+            'has_commission' => $hasCommission,
+            'commission_amount' => $commissionAmount
+        ]);
+    }
+
+    /**
+     * 获取订单详情
+     */
+    public function orderDetail(): void
+    {
+        try {
+            $userId = $this->auth->id;
+            $orderId = $this->request->get('order_id', 0);
+            $orderNo = $this->request->get('order_no', '');
+
+            if (empty($orderId) && empty($orderNo)) {
+                $this->error('参数错误');
+            }
+
+            // 查询订单
+            if ($orderId) {
+                $order = Db::table('ba_order')
+                    ->where('id', $orderId)
+                    ->where('user_id', $userId)
+                    ->find();
+            } else {
+                $order = Db::table('ba_order')
+                    ->where('order_no', $orderNo)
+                    ->where('user_id', $userId)
+                    ->find();
+            }
+
+            if (!$order) {
+                $this->error('订单不存在');
+            }
+
+            // 查询订单商品
+            $orderItem = Db::table('ba_order_item')
+                ->where('order_id', $order['id'])
+                ->find();
+
+            if ($orderItem) {
+                // 查询套餐详情
+                $package = Db::table('ba_package_price')
+                    ->where('id', $orderItem['package_price_id'])
+                    ->find();
+
+                if ($package) {
+                    $orderItem['package_info'] = [
+                        'name' => $package['spec_name'],
+                        'price' => $package['price'],
+                        'type' => $package['type'],
+                        'type_text' => $package['type'] == 1 ? '个人' : '企业'
+                    ];
+                }
+
+                // 查询产品信息
+                $product = Db::table('ba_product')
+                    ->where('id', $orderItem['product_id'])
+                    ->field('id, name')
+                    ->find();
+
+                if ($product) {
+                    $orderItem['product_info'] = $product;
+                }
+
+                $order['item'] = $orderItem;
+            }
+
+            // 格式化状态
+            $order['pay_status_text'] = $this->getPayStatusText($order['pay_status']);
+            $order['order_status_text'] = $this->getOrderStatusText($order['order_status']);
+
+            // 格式化时间
+            $order['order_time_formatted'] = $order['order_time']
+                ? date('m-d H:i', strtotime($order['order_time']))
+                : '';
+            $order['pay_time_formatted'] = $order['pay_time']
+                ? date('m-d H:i', strtotime($order['pay_time']))
+                : '';
+
+            $this->success('', $order);
+
+        } catch (\Throwable $e) {
+            \think\facade\Log::error('获取订单详情失败: ' . $e->getMessage());
+            $this->error('获取订单详情失败');
+        }
+    }
+
+    /**
+     * 获取支付状态文本
+     */
+    private function getPayStatusText($status): string
+    {
+        $texts = [
+            0 => '待支付',
+            1 => '已支付',
+            2 => '支付失败'
+        ];
+        return $texts[$status] ?? '未知';
+    }
+
+    /**
+     * 获取订单状态文本
+     */
+    private function getOrderStatusText($status): string
+    {
+        $texts = [
+            0 => '待处理',
+            1 => '已确认',
+            2 => '已发货',
+            3 => '已完成',
+            4 => '已取消'
+        ];
+        return $texts[$status] ?? '未知';
+    }
+}

+ 1 - 1
app/api/validate/User.php

@@ -10,7 +10,7 @@ class User extends Validate
     protected $failException = true;
 
     protected $rule = [
-        'username'     => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
+        'username'     => 'require|regex:/^[\s\S]{2,15}$/|unique:user',
         'password'     => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
         'registerType' => 'require|in:email,mobile',
         'email'        => 'email|unique:user|requireIf:registerType,email',

+ 1 - 1
app/common/library/Auth.php

@@ -181,7 +181,7 @@ class Auth extends \ba\Auth
         $validate = Validate::rule([
             'email|' . __('Email')       => 'email|unique:user',
             'mobile|' . __('Mobile')     => 'mobile|unique:user',
-            'username|' . __('Username') => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
+            'username|' . __('Username') => 'require|regex:/^[\s\S]{2,15}$/|unique:user',
             'password|' . __('Password') => 'regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
         ]);
         $params   = [

+ 314 - 0
app/common/service/AlipayPaymentService.php

@@ -0,0 +1,314 @@
+<?php
+
+namespace app\common\service;
+
+class AlipayPaymentService extends BasePaymentService
+{
+    // 支付宝API地址
+    protected $apiUrl = 'https://openapi.alipay.com/gateway.do';
+
+    // 配置参数
+    protected $config = [];
+
+    /**
+     * 初始化支付宝配置
+     */
+    protected function initConfig()
+    {
+        $this->config = [
+            'app_id' => $this->payConfig['app_id'],
+            'merchant_private_key' => $this->getPrivateKey(),
+            'alipay_public_key' => $this->getPublicKey(),
+            'notify_url' => $this->payConfig['notify_url'] ?: url('/api/invitation/payment/alipayNotify', [], true, true)->build(),
+            'charset' => 'UTF-8',
+            'sign_type' => 'RSA2',
+            'gateway_url' => $this->apiUrl,
+        ];
+
+        if (empty($this->config['app_id']) || empty($this->config['merchant_private_key'])) {
+            throw new \Exception('支付宝支付配置不完整');
+        }
+    }
+
+    /**
+     * 获取私钥
+     */
+    protected function getPrivateKey()
+    {
+        // 从文件路径读取或直接使用配置中的私钥
+        if (!empty($this->payConfig['private_key_path']) && file_exists($this->payConfig['private_key_path'])) {
+            return file_get_contents($this->payConfig['private_key_path']);
+        }
+
+        // 如果没有文件路径,可能私钥直接存储在配置中
+        return $this->payConfig['key'] ?? '';
+    }
+
+    /**
+     * 获取公钥
+     */
+    protected function getPublicKey()
+    {
+        if (!empty($this->payConfig['public_key_path']) && file_exists($this->payConfig['public_key_path'])) {
+            return file_get_contents($this->payConfig['public_key_path']);
+        }
+
+        return '';
+    }
+
+    /**
+     * 生成支付二维码
+     */
+    public function createQrCode($orderNo, $amount, $subject, $body = '')
+    {
+        try {
+            // 业务参数
+            $bizContent = [
+                'out_trade_no' => $orderNo,
+                'total_amount' => number_format($amount, 2, '.', ''),
+                'subject' => $subject,
+                'body' => $body ?: $subject,
+                'product_code' => 'FAST_INSTANT_TRADE_PAY',
+            ];
+
+            // 公共参数
+            $params = [
+                'app_id' => $this->config['app_id'],
+                'method' => 'alipay.trade.precreate',
+                'charset' => $this->config['charset'],
+                'sign_type' => $this->config['sign_type'],
+                'timestamp' => date('Y-m-d H:i:s'),
+                'version' => '1.0',
+                'notify_url' => $this->config['notify_url'],
+                'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE),
+            ];
+
+            // 生成签名
+            $params['sign'] = $this->generateSign($params);
+
+            // 发送请求
+            $result = $this->execute($params);
+
+            $response = $result['alipay_trade_precreate_response'];
+
+            if ($response['code'] == '10000') {
+                return [
+                    'qr_code_url' => $response['qr_code'],
+                    'qr_code_content' => $response['qr_code'],
+                    'out_request_no' => $response['out_trade_no'],
+                ];
+            } else {
+                $errorMsg = $response['sub_msg'] ?? $response['msg'] ?? '支付宝下单失败';
+                throw new \Exception($errorMsg);
+            }
+
+        } catch (\Throwable $e) {
+            $this->log('生成支付宝支付二维码失败', [
+                'order_no' => $orderNo,
+                'error' => $e->getMessage()
+            ], 'error');
+            throw $e;
+        }
+    }
+
+    /**
+     * 处理支付回调
+     */
+    public function handleNotify($input)
+    {
+        try {
+            // 验证签名
+            if (!$this->verifySign($input)) {
+                throw new \Exception('签名验证失败');
+            }
+
+            // 检查交易状态
+            $tradeStatus = $input['trade_status'];
+            if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
+                throw new \Exception('支付未成功: ' . $tradeStatus);
+            }
+
+            return [
+                'order_no' => $input['out_trade_no'],
+                'trade_no' => $input['trade_no'],
+                'pay_amount' => $input['total_amount'],
+                'pay_time' => $input['gmt_payment'] ?? date('Y-m-d H:i:s'),
+                'raw_data' => $input,
+            ];
+
+        } catch (\Throwable $e) {
+            $this->log('处理支付宝支付回调失败', [
+                'input' => $input,
+                'error' => $e->getMessage()
+            ], 'error');
+            throw $e;
+        }
+    }
+
+    /**
+     * 查询支付订单
+     */
+    public function queryOrder($orderNo)
+    {
+        try {
+            // 业务参数
+            $bizContent = [
+                'out_trade_no' => $orderNo,
+            ];
+
+            // 公共参数
+            $params = [
+                'app_id' => $this->config['app_id'],
+                'method' => 'alipay.trade.query',
+                'charset' => $this->config['charset'],
+                'sign_type' => $this->config['sign_type'],
+                'timestamp' => date('Y-m-d H:i:s'),
+                'version' => '1.0',
+                'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE),
+            ];
+
+            $params['sign'] = $this->generateSign($params);
+
+            $result = $this->execute($params);
+            $response = $result['alipay_trade_query_response'];
+
+            if ($response['code'] == '10000') {
+                $tradeStatus = $response['trade_status'];
+                $payStatus = 0; // 待支付
+
+                if (in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
+                    $payStatus = 1; // 已支付
+                } elseif ($tradeStatus == 'TRADE_CLOSED') {
+                    $payStatus = 2; // 支付失败
+                }
+
+                return [
+                    'order_no' => $response['out_trade_no'],
+                    'trade_no' => $response['trade_no'],
+                    'pay_status' => $payStatus,
+                    'pay_amount' => $response['total_amount'],
+                    'pay_time' => $response['send_pay_date'] ?? null,
+                    'trade_status' => $tradeStatus,
+                ];
+            } elseif ($response['code'] == '40004') {
+                // 订单不存在,可能是未支付
+                return [
+                    'order_no' => $orderNo,
+                    'pay_status' => 0,
+                    'trade_status' => 'WAIT_BUYER_PAY',
+                ];
+            }
+
+            throw new \Exception('查询订单失败: ' . ($response['sub_msg'] ?? $response['msg'] ?? '未知错误'));
+
+        } catch (\Throwable $e) {
+            $this->log('查询支付宝支付订单失败', [
+                'order_no' => $orderNo,
+                'error' => $e->getMessage()
+            ], 'error');
+            throw $e;
+        }
+    }
+
+    /**
+     * 验证签名
+     */
+    protected function verifySign($data)
+    {
+        $sign = $data['sign'];
+        $signType = $data['sign_type'];
+
+        // 移除sign和sign_type参数
+        unset($data['sign'], $data['sign_type']);
+
+        // 参数排序
+        ksort($data);
+
+        // 拼接待签名字符串
+        $signContent = '';
+        foreach ($data as $key => $value) {
+            if ($value !== '' && $value !== null && $key != 'sign') {
+                $signContent .= $key . '=' . $value . '&';
+            }
+        }
+        $signContent = rtrim($signContent, '&');
+
+        // 验证签名
+        $publicKey = $this->config['alipay_public_key'];
+        $res = openssl_get_publickey($publicKey);
+        $result = openssl_verify($signContent, base64_decode($sign), $res, OPENSSL_ALGO_SHA256);
+        openssl_free_key($res);
+
+        return $result === 1;
+    }
+
+    /**
+     * 生成签名
+     */
+    protected function generateSign($data)
+    {
+        // 参数排序
+        ksort($data);
+
+        // 拼接待签名字符串
+        $signContent = '';
+        foreach ($data as $key => $value) {
+            if ($value !== '' && $value !== null && $key != 'sign') {
+                $signContent .= $key . '=' . $value . '&';
+            }
+        }
+        $signContent = rtrim($signContent, '&');
+
+        // 使用私钥生成签名
+        $privateKey = $this->config['merchant_private_key'];
+        $res = openssl_get_privatekey($privateKey);
+
+        if (!$res) {
+            throw new \Exception('私钥格式错误');
+        }
+
+        if ($this->config['sign_type'] == 'RSA2') {
+            openssl_sign($signContent, $sign, $res, OPENSSL_ALGO_SHA256);
+        } else {
+            openssl_sign($signContent, $sign, $res);
+        }
+
+        openssl_free_key($res);
+
+        return base64_encode($sign);
+    }
+
+    /**
+     * 执行请求
+     */
+    private function execute($params)
+    {
+        $url = $this->config['gateway_url'];
+
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+
+        $response = curl_exec($ch);
+
+        if (curl_errno($ch)) {
+            $error = curl_error($ch);
+            curl_close($ch);
+            throw new \Exception('CURL错误: ' . $error);
+        }
+
+        curl_close($ch);
+
+        $result = json_decode($response, true);
+        if (json_last_error() !== JSON_ERROR_NONE) {
+            throw new \Exception('JSON解析失败: ' . json_last_error_msg());
+        }
+
+        return $result;
+    }
+}

+ 96 - 0
app/common/service/BasePaymentService.php

@@ -0,0 +1,96 @@
+<?php
+
+namespace app\common\service;
+
+use think\facade\Db;
+use think\facade\Log;
+
+abstract class BasePaymentService
+{
+    // 支付配置
+    protected $config;
+
+    // 支付方式
+    protected $payMethod;
+
+    // 支付平台配置
+    protected $payConfig;
+
+    /**
+     * 初始化支付服务
+     */
+    public function __construct($payMethod, $payConfigId = 0)
+    {
+        $this->payMethod = $payMethod;
+
+        // 获取支付配置
+        $this->payConfig = $this->getPayConfig($payMethod, $payConfigId);
+        if (!$this->payConfig) {
+            throw new \Exception('支付配置不存在或已禁用');
+        }
+
+        // 初始化支付平台配置
+        $this->initConfig();
+    }
+
+    /**
+     * 获取支付配置
+     */
+    public function getPayConfig($payMethod, $payConfigId = 0)
+    {
+        $query = Db::table('ba_pay_config')
+            ->where('type', $payMethod == 'wechat' ? 1 : 2)
+            ->where('status', 1);
+
+        if ($payConfigId > 0) {
+            $query->where('id', $payConfigId);
+        }
+
+        return $query->order('id', 'desc')->find();
+    }
+
+    /**
+     * 初始化配置(子类实现)
+     */
+    abstract protected function initConfig();
+
+    /**
+     * 生成支付二维码
+     */
+    abstract public function createQrCode($orderNo, $amount, $subject, $body = '');
+
+    /**
+     * 处理支付回调
+     */
+    abstract public function handleNotify($input);
+
+    /**
+     * 查询支付订单
+     */
+    abstract public function queryOrder($orderNo);
+
+    /**
+     * 验证签名
+     */
+    abstract protected function verifySign($data, $sign);
+
+    /**
+     * 生成签名
+     */
+    abstract protected function generateSign($data);
+
+    /**
+     * 记录支付日志
+     */
+    protected function log($message, $data = [], $level = 'info')
+    {
+        $logData = [
+            'message' => $message,
+            'data' => $data,
+            'pay_method' => $this->payMethod,
+            'time' => date('Y-m-d H:i:s')
+        ];
+
+        Log::write("支付服务: " . json_encode($logData), $level);
+    }
+}

+ 504 - 0
app/common/service/CommissionService.php

@@ -0,0 +1,504 @@
+<?php
+
+namespace app\common\service;
+
+use think\facade\Db;
+use think\facade\Log;
+
+class CommissionService
+{
+
+
+    /**
+     * 获取分销设置
+     */
+    public function getDistributionSetting()
+    {
+        $setting = Db::table('ba_distribution_setting')
+            ->order('id', 'desc')
+            ->find();
+        // 如果没有设置,返回默认值
+        if (!$setting) {
+            $setting = [
+                'default_rate' => 0.10, // 10%
+                'min_withdrawal' => 10.00,
+                'max_withdrawal' => 10000.00,
+                'withdrawal_fee_rate' => 0.01, // 1%
+            ];
+        }
+
+        return $setting;
+    }
+
+    /**
+     * 计算佣金
+     */
+    public function calculateCommission($orderAmount, $distributorId = 0)
+    {
+        if ($distributorId <= 0) {
+            return [
+                'has_commission' => false,
+                'commission_rate' => 0,
+                'commission_amount' => 0
+            ];
+        }
+
+        $setting = $this->getDistributionSetting();
+        $commissionRate = ($setting['default_rate']/100) ?? 0.10;
+
+        // 计算佣金金额
+        $commissionAmount = bcmul($orderAmount, $commissionRate, 2);
+
+        return [
+            'has_commission' => true,
+            'commission_rate' => $commissionRate,
+            'commission_amount' => $commissionAmount,
+            'setting' => $setting
+        ];
+    }
+
+
+    /**
+     * 创建分销记录
+     */
+    public function createDistributionRecord($data)
+    {
+        $recordData = [
+            'order_id' => $data['order_id'] ?? 0,
+            'product_id' => $data['product_id'] ?? 0,
+            'buyer_id' => $data['buyer_id'] ?? 0,
+            'distributor_id' => $data['distributor_id'] ?? 0,
+            'commission_rate' => $data['commission_rate'] ?? 0,
+            'commission_amount' => $data['commission_amount'] ?? 0,
+            'status' => 1, // 1=待结算
+            'create_time' => date('Y-m-d H:i:s')
+        ];
+
+        return Db::table('ba_distribution_record')->insertGetId($recordData);
+    }
+
+    /**
+     * 更新分销记录状态
+     */
+    public function updateDistributionRecord($orderId, $status, $settleTime = null)
+    {
+        $updateData = [
+            'status' => $status,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        if ($settleTime) {
+            $updateData['settle_time'] = $settleTime;
+        }
+
+        return Db::table('ba_distribution_record')
+            ->where('order_id', $orderId)
+            ->update($updateData);
+    }
+
+    /**
+     * 获取用户佣金统计
+     */
+    public function getUserCommissionSummary($userId)
+    {
+        $summary = Db::table('ba_distribution_record')
+            ->where('distributor_id', $userId)
+            ->field('
+                COUNT(*) as total_orders,
+                SUM(CASE WHEN status = 1 THEN commission_amount ELSE 0 END) as pending_amount,
+                SUM(CASE WHEN status = 2 THEN commission_amount ELSE 0 END) as settled_amount,
+                SUM(CASE WHEN status = 3 THEN commission_amount ELSE 0 END) as canceled_amount
+            ')
+            ->find();
+
+        return $summary ?: [
+            'total_orders' => 0,
+            'pending_amount' => 0,
+            'settled_amount' => 0,
+            'canceled_amount' => 0
+        ];
+    }
+
+    /**
+     * 更新用户佣金表
+     */
+    public function updateUserCommission($userId)
+    {
+        $summary = $this->getUserCommissionSummary($userId);
+
+        // 已结算的佣金可以作为当前佣金(可提现)
+        $currentCommission = $summary['settled_amount'] ?? 0;
+        $totalCommission = $summary['settled_amount'] + $summary['pending_amount'];
+
+        // 检查用户佣金表是否有记录
+        $userCommission = Db::table('ba_user_commission')
+            ->where('user_id', $userId)
+            ->find();
+
+        $data = [
+            'total_commission' => $totalCommission,
+            'current_commission' => $currentCommission,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        if ($userCommission) {
+            // 更新
+            Db::table('ba_user_commission')
+                ->where('user_id', $userId)
+                ->update($data);
+        } else {
+            // 创建
+            $data['user_id'] = $userId;
+            $data['promotion_link'] = $this->generatePromotionLink($userId);
+            Db::table('ba_user_commission')->insert($data);
+        }
+    }
+
+
+
+    /**
+     * 处理订单佣金
+     */
+    public function processOrderCommission(int $orderId): bool
+    {
+        try {
+            // 获取订单信息
+            $order = Db::name('order')
+                ->where('id', $orderId)
+                ->where('pay_status', 1) // 已支付
+                ->where('order_status', 'IN', [1, 3]) // 已确认或已完成
+                ->where('is_commission', 0) // 未处理佣金
+                ->find();
+
+            if (!$order) {
+                Log::info("订单不满足佣金处理条件: {$orderId}");
+                return false;
+            }
+
+            // 如果有分销人,处理佣金
+            if (!empty($order['distributor_id']) && $order['commission_amount'] > 0) {
+                $distributorId = $order['distributor_id'];
+                $commissionAmount = $order['commission_amount'];
+
+                // 更新分销人佣金
+                $this->addCommission($distributorId, $commissionAmount, [
+                    'type' => 2, // 订单佣金
+                    'order_id' => $orderId,
+                    'from_user_id' => $order['user_id'],
+                    'remark' => '订单佣金:' . $order['order_no'] . ',金额:¥' . $commissionAmount
+                ]);
+
+                // 标记订单佣金已处理
+                Db::name('order')
+                    ->where('id', $orderId)
+                    ->update([
+                        'is_commission' => 1,
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+
+                Log::info("订单佣金处理成功: 订单ID={$orderId}, 分销人ID={$distributorId}, 佣金={$commissionAmount}");
+            }
+
+            return true;
+        } catch (\Throwable $e) {
+            Log::error("处理订单佣金失败: " . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 增加佣金
+     */
+    public function addCommission(int $userId, float $amount, array $extra = []): bool
+    {
+        try {
+            if ($amount <= 0) {
+                return false;
+            }
+
+            Db::startTrans();
+
+            // 获取当前佣金
+            $commission = Db::name('user_commission')
+                ->where('user_id', $userId)
+                ->lock(true)
+                ->find();
+
+            $balanceBefore = $commission ? $commission['current_commission'] : 0;
+            $balanceAfter = $balanceBefore + $amount;
+
+            if ($commission) {
+                // 更新现有记录
+                Db::name('user_commission')
+                    ->where('user_id', $userId)
+                    ->update([
+                        'total_commission' => Db::raw('total_commission + ' . $amount),
+                        'current_commission' => Db::raw('current_commission + ' . $amount),
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            } else {
+                // 创建新记录
+                $promotionLink = $this->generatePromotionLink($userId);
+
+                Db::name('user_commission')->insert([
+                    'user_id' => $userId,
+                    'total_commission' => $amount,
+                    'current_commission' => $amount,
+                    'withdrawn_commission' => 0.00,
+                    'frozen_commission' => 0.00,
+                    'promotion_link' => $promotionLink,
+                    'invitation_count' => 0,
+                    'total_invitation' => 0,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+            }
+
+            // 记录佣金流水
+            Db::name('commission_flow')->insert([
+                'user_id' => $userId,
+                'order_id' => $extra['order_id'] ?? null,
+                'from_user_id' => $extra['from_user_id'] ?? null,
+                'type' => $extra['type'] ?? 1,
+                'amount' => $amount,
+                'balance_before' => $balanceBefore,
+                'balance_after' => $balanceAfter,
+                'remark' => $extra['remark'] ?? ('佣金增加:¥' . $amount),
+                'create_time' => date('Y-m-d H:i:s')
+            ]);
+
+            Db::commit();
+            return true;
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error("增加佣金失败: " . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 处理提现
+     */
+    public function processWithdrawal(int $withdrawalId): bool
+    {
+        try {
+            $withdrawal = Db::name('withdrawal_record')
+                ->where('id', $withdrawalId)
+                ->where('status', 0) // 待审核
+                ->lock(true)
+                ->find();
+
+            if (!$withdrawal) {
+                Log::warning("提现记录不存在或状态不符: {$withdrawalId}");
+                return false;
+            }
+
+            $userId = $withdrawal['user_id'];
+            $amount = $withdrawal['amount'];
+
+            // 检查用户佣金是否足够
+            $commission = Db::name('user_commission')
+                ->where('user_id', $userId)
+                ->find();
+
+            if (!$commission || $commission['current_commission'] < $amount) {
+                Log::warning("用户佣金不足: 用户ID={$userId}, 需要={$amount}, 当前={$commission['current_commission']}");
+                return false;
+            }
+
+            Db::startTrans();
+
+            // 扣除佣金
+            $newCurrent = $commission['current_commission'] - $amount;
+            $newWithdrawn = $commission['withdrawn_commission'] + $amount;
+
+            Db::name('user_commission')
+                ->where('user_id', $userId)
+                ->update([
+                    'current_commission' => $newCurrent,
+                    'withdrawn_commission' => $newWithdrawn,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            // 记录佣金流水
+            Db::name('commission_flow')->insert([
+                'user_id' => $userId,
+                'type' => 3, // 提现扣除
+                'amount' => -$amount,
+                'balance_before' => $commission['current_commission'],
+                'balance_after' => $newCurrent,
+                'remark' => '提现扣除:' . $withdrawal['withdrawal_no'] . ',金额:¥' . $amount,
+                'create_time' => date('Y-m-d H:i:s')
+            ]);
+
+            // 更新提现记录状态为审核通过(实际打款需要另外处理)
+            Db::name('withdrawal_record')
+                ->where('id', $withdrawalId)
+                ->update([
+                    'status' => 1, // 审核通过
+                    'audit_time' => date('Y-m-d H:i:s'),
+                    'current_balance' => $commission['current_commission'],
+                    'balance_after' => $newCurrent,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            Db::commit();
+
+            Log::info("提现处理成功: 提现ID={$withdrawalId}, 用户ID={$userId}, 金额={$amount}");
+
+            return true;
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error("处理提现失败: " . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 获取用户佣金详情
+     */
+    public function getUserCommissionDetail(int $userId): array
+    {
+        try {
+            // 佣金信息
+            $commission = Db::name('user_commission')
+                ->where('user_id', $userId)
+                ->find();
+
+            if (!$commission) {
+                $commission = [
+                    'total_commission' => 0.00,
+                    'current_commission' => 0.00,
+                    'withdrawn_commission' => 0.00,
+                    'frozen_commission' => 0.00,
+                    'promotion_link' => $this->generatePromotionLink($userId),
+                    'invitation_count' => 0,
+                    'total_invitation' => 0
+                ];
+            }
+
+            // 最近佣金流水
+            $flows = Db::name('commission_flow')
+                ->where('user_id', $userId)
+                ->order('create_time', 'desc')
+                ->limit(50)
+                ->select()
+                ->toArray();
+
+            // 统计今日/本月佣金
+            $todayStart = date('Y-m-d 00:00:00');
+            $monthStart = date('Y-m-01 00:00:00');
+
+            $todayIncome = Db::name('commission_flow')
+                ->where('user_id', $userId)
+                ->where('amount', '>', 0)
+                ->where('create_time', '>=', $todayStart)
+                ->sum('amount');
+
+            $monthIncome = Db::name('commission_flow')
+                ->where('user_id', $userId)
+                ->where('amount', '>', 0)
+                ->where('create_time', '>=', $monthStart)
+                ->sum('amount');
+
+            return [
+                'commission' => $commission,
+                'flows' => $flows,
+                'stats' => [
+                    'today_income' => $todayIncome ?: 0,
+                    'month_income' => $monthIncome ?: 0
+                ]
+            ];
+        } catch (\Throwable $e) {
+            Log::error("获取用户佣金详情失败: " . $e->getMessage());
+            return [
+                'commission' => [
+                    'total_commission' => 0.00,
+                    'current_commission' => 0.00,
+                    'withdrawn_commission' => 0.00,
+                    'frozen_commission' => 0.00,
+                    'promotion_link' => '',
+                    'invitation_count' => 0,
+                    'total_invitation' => 0
+                ],
+                'flows' => [],
+                'stats' => [
+                    'today_income' => 0,
+                    'month_income' => 0
+                ]
+            ];
+        }
+    }
+
+
+    /**
+     * 生成唯一邀请码
+     */
+    public function generateInvitationCode(): string
+    {
+        $maxAttempts = 10;
+        $attempts = 0;
+
+        while ($attempts < $maxAttempts) {
+            // 方法1:使用数字+字母组合
+            $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // 去掉容易混淆的字符
+            $code = '';
+            for ($i = 0; $i < 6; $i++) {
+                $code .= $chars[mt_rand(0, strlen($chars) - 1)];
+            }
+
+            // 方法2:或者使用更短的邀请码
+            // $code = strtoupper(substr(md5(uniqid()), 0, 6));
+
+            // 检查邀请码是否已存在
+            $exists = Db::table('ba_user')
+                ->where('invitation_code', $code)
+                ->count();
+
+            if (!$exists) {
+                return $code;
+            }
+
+            $attempts++;
+
+            // 如果冲突,尝试不同的算法
+            if ($attempts >= 5) {
+                $code = 'INV' . mt_rand(1000, 9999);
+                $exists = Db::table('ba_user')
+                    ->where('invitation_code', $code)
+                    ->count();
+
+                if (!$exists) {
+                    return $code;
+                }
+            }
+        }
+
+        // 如果还是冲突,使用时间戳+随机数
+        return 'INV' . time() . mt_rand(10, 99);
+    }
+
+    /**
+     * 生成推广链接
+     */
+    private function generatePromotionLink(int $userId): string
+    {
+        // 获取用户的邀请码
+        $user = Db::table('ba_user')
+            ->where('id', $userId)
+            ->field('invitation_code')
+            ->find();
+
+        $invitationCode = $user['invitation_code'] ?? '';
+
+        if (empty($invitationCode)) {
+            // 如果用户还没有邀请码,先生成一个
+            $invitationCode = $this->generateInvitationCode();
+            Db::table('ba_user')
+                ->where('id', $userId)
+                ->update(['invitation_code' => $invitationCode]);
+        }
+
+        $domain = request()->domain();
+        return $domain . '/#/user/register?invite=' . $invitationCode;
+    }
+}

+ 1414 - 0
app/common/service/CompanyPackageService.php

@@ -0,0 +1,1414 @@
+<?php
+namespace app\common\service;
+
+use think\facade\Db;
+use think\facade\Log;
+
+class CompanyPackageService
+{
+    /**
+     * 验证企业套餐购买资格
+     */
+    public function validateCompanyPurchase($userId, $packagePriceId, $quantity)
+    {
+        try {
+            // 1. 获取套餐信息
+            $package = Db::table('ba_package_price')
+                ->alias('pp')
+                ->leftJoin('ba_package p', 'p.id = pp.package_id')
+                ->where('pp.id', $packagePriceId)
+                ->field('pp.*, p.name as package_name')
+                ->find();
+
+            if (!$package) {
+                throw new \Exception('套餐不存在');
+            }
+
+            // 2. 如果是企业套餐,检查用户是否属于企业
+            if ($package['type'] == 2) { // 企业套餐
+                $companyInfo = $this->getUserCompany($userId);
+                if (!$companyInfo) {
+                    throw new \Exception('只有企业成员才能购买企业套餐');
+                }
+
+                // 3. 检查日期卡的最低购买数量
+                if ($package['is_date_card'] == 1 && $quantity < $package['min_quantity']) {
+                    throw new \Exception('企业日期卡至少需要购买' . $package['min_quantity'] . '份');
+                }
+
+                return [
+                    'valid' => true,
+                    'company_id' => $companyInfo['id'],
+                    'company_name' => $companyInfo['name'],
+                    'min_quantity' => $package['min_quantity']
+                ];
+            }
+
+            return ['valid' => true, 'company_id' => null];
+
+        } catch (\Throwable $e) {
+            Log::error('验证企业套餐购买资格失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * 获取用户所在企业
+     */
+    public function getUserCompany($userId)
+    {
+        // 检查用户是否是企业创建人或成员
+        $company = Db::table('ba_company')
+            ->where('creator_id', $userId)
+            ->where('status', 1)
+            ->field('id, name,creator_id')
+            ->find();
+        if ($company) {
+            return $company;
+        }
+
+        // 检查是否是成员
+        $member = Db::table('ba_company_member')
+            ->alias('cm')
+            ->leftJoin('ba_company c', 'c.id = cm.company_id')
+            ->where('cm.user_id', $userId)
+            ->where('cm.status', 1)
+            ->where('c.status', 1)
+            ->field('c.id, c.name')
+            ->find();
+
+        return $member;
+    }
+
+    /**
+     * 处理企业套餐购买(创建席位记录)
+     */
+    public function processCompanyPackagePurchase($orderId, $companyId, $packagePriceId, $quantity, $cardType, $unitValue, $pointsValue)
+    {
+        Db::startTrans();
+        try {
+            // 1. 获取或创建企业套餐席位记录
+            $seatsRecord = Db::table('ba_company_package_seats')
+                ->where('company_id', $companyId)
+                ->where('package_price_id', $packagePriceId)
+                ->find();
+
+            if ($seatsRecord) {
+                // 更新现有记录
+                $updateData = [
+                    'total_seats' => Db::raw('total_seats + ' . $quantity),
+                    'available_seats' => Db::raw('available_seats + ' . $quantity),
+                    'is_shared' => $cardType < 3 ? 1 : 0, // 次卡/积分卡为共享资源
+                    'update_time' => date('Y-m-d H:i:s')
+                ];
+
+                // 如果是次卡,累加次数
+                if ($cardType == 1) {
+                    $updateData['total_times'] = Db::raw('total_times + ' . ($unitValue * $quantity));
+                    $updateData['remaining_times'] = Db::raw('remaining_times + ' . ($unitValue * $quantity));
+                }
+
+                // 如果是积分卡,累加积分
+                if ($cardType == 2) {
+                    $updateData['total_points'] = Db::raw('total_points + ' . ($pointsValue * $quantity));
+                    $updateData['remaining_points'] = Db::raw('remaining_points + ' . ($pointsValue * $quantity));
+                }
+
+                Db::table('ba_company_package_seats')
+                    ->where('id', $seatsRecord['id'])
+                    ->update($updateData);
+                $seatsId = $seatsRecord['id'];
+            } else {
+                // 创建新记录
+                $seatsData = [
+                    'company_id' => $companyId,
+                    'package_price_id' => $packagePriceId,
+                    'product_id' => $this->getProductIdByPackage($packagePriceId),
+                    'total_seats' => $quantity,
+                    'assigned_seats' => 0,
+                    'available_seats' => $quantity,
+                    'is_shared' => $cardType < 3 ? 1 : 0, // 次卡/积分卡为共享资源
+                    'card_type' => $cardType,
+                    'total_times' => $cardType == 1 ? $unitValue * $quantity : 0,
+                    'remaining_times' => $cardType == 1 ? $unitValue * $quantity : 0,
+                    'total_points' => $cardType == 2 ? $pointsValue * $quantity : 0,
+                    'remaining_points' => $cardType == 2 ? $pointsValue * $quantity : 0,
+                    'status' => 1,
+                    'create_time' => date('Y-m-d H:i:s'),
+                    'update_time' => date('Y-m-d H:i:s')
+                ];
+
+                $seatsId = Db::table('ba_company_package_seats')->insertGetId($seatsData);
+            }
+
+            // 2. 获取购买者ID
+            $buyerId = $this->getOrderBuyerId($orderId);
+
+            // 3. 查询订单商品
+            $orderItems = Db::table('ba_order_item')
+                ->where('order_id', $orderId)
+                ->where('package_price_id', $packagePriceId)
+                ->select();
+
+            foreach ($orderItems as $item) {
+                if ($cardType >= 3) {
+                    // 日期卡:为每个购买的数量创建独立的分配记录
+                    for ($i = 0; $i < $quantity; $i++) {
+                        $allocationData = [
+                            'order_item_id' => $item['id'],
+                            'order_id' => $orderId,
+                            'package_price_id' => $packagePriceId,
+                            'product_id' => $item['product_id'],
+                            'buyer_id' => $buyerId,
+                            'company_id' => $companyId,
+                            'assigned_to_user_id' => null, // 初始不分配
+                            'assigned_by_user_id' => $buyerId,
+                            'record_type' => 2, // 购买记录
+                            'is_general' => 0,
+                            'seat_index' => $i + 1, // 席位索引
+                            'allocation_status' => 0, // 未分配
+                            'allocation_time' => null,
+                            'start_time' => null,
+                            'end_time' => null,
+                            'expire_time' => $this->calculateEndTime($cardType, date('Y-m-d H:i:s')), // 冗余字段
+                            'remaining_times' => 0, // 日期卡没有次数
+                            'remaining_points' => 0, // 日期卡没有积分
+                            'is_used' => 0,
+                            'create_time' => date('Y-m-d H:i:s'),
+                            'update_time' => date('Y-m-d H:i:s')
+                        ];
+
+                        $allocationId = Db::table('ba_company_package_allocation')->insertGetId($allocationData);
+                    }
+                } else {
+                    // 次卡和积分卡:
+                    // 1. 创建通用记录(用于管理总资源)
+                    $generalAllocationData = [
+                        'order_item_id' => $item['id'],
+                        'order_id' => $orderId,
+                        'package_price_id' => $packagePriceId,
+                        'product_id' => $item['product_id'],
+                        'buyer_id' => $buyerId,
+                        'company_id' => $companyId,
+                        'assigned_to_user_id' => null, // 通用记录不分配
+                        'assigned_by_user_id' => $buyerId,
+                        'record_type' => 0, // 通用记录
+                        'is_general' => 1, // 是通用记录
+                        'seat_index' => 0,
+                        'allocation_status' => 0, // 状态:通用记录
+                        'allocation_time' => null,
+                        'start_time' => null,
+                        'end_time' => null,
+                        'expire_time' => null,
+                        'remaining_times' => $cardType == 1 ? $unitValue * $quantity : 0,
+                        'remaining_points' => $cardType == 2 ? $pointsValue * $quantity : 0,
+                        'is_used' => 0,
+                        'create_time' => date('Y-m-d H:i:s'),
+                        'update_time' => date('Y-m-d H:i:s')
+                    ];
+
+                    $generalAllocationId = Db::table('ba_company_package_allocation')->insertGetId($generalAllocationData);
+
+                    // 2. 为购买者创建独立的分配记录
+                    $buyerAllocationData = [
+                        'order_item_id' => $item['id'],
+                        'order_id' => $orderId,
+                        'package_price_id' => $packagePriceId,
+                        'product_id' => $item['product_id'],
+                        'buyer_id' => $buyerId,
+                        'company_id' => $companyId,
+                        'assigned_to_user_id' => $buyerId,
+                        'assigned_by_user_id' => $buyerId,
+                        'record_type' => 1, // 用户分配记录
+                        'is_general' => 0,
+                        'seat_index' => 0,
+                        'parent_id' => $generalAllocationId, // 关联通用记录
+                        'allocation_status' => 1, // 已分配
+                        'allocation_time' => date('Y-m-d H:i:s'),
+                        'start_time' => date('Y-m-d H:i:s'),
+                        'end_time' => null,
+                        'expire_time' => null,
+                        'remaining_times' => $cardType == 1 ? $unitValue : 0,
+                        'remaining_points' => $cardType == 2 ? $pointsValue : 0,
+                        'is_used' => 0,
+                        'create_time' => date('Y-m-d H:i:s'),
+                        'update_time' => date('Y-m-d H:i:s')
+                    ];
+
+                    $buyerAllocationId = Db::table('ba_company_package_allocation')->insertGetId($buyerAllocationData);
+
+                    // 3. 将购买者添加到授权列表
+                    $seats = Db::table('ba_company_package_seats')
+                        ->where('id', $seatsId)
+                        ->find();
+
+                    $authorizedUsers = json_decode($seats['authorized_users'] ?? '[]', true);
+
+                    if (!in_array($buyerId, $authorizedUsers)) {
+                        $authorizedUsers[] = $buyerId;
+
+                        Db::table('ba_company_package_seats')
+                            ->where('id', $seatsId)
+                            ->update([
+                                'authorized_users' => json_encode($authorizedUsers),
+                                'assigned_member_count' => Db::raw('assigned_member_count + 1')
+                            ]);
+                    }
+                }
+
+                // 更新订单商品的分配记录ID(对于日期卡,记录第一个分配记录的ID)
+                if ($cardType >= 3) {
+                    // 获取第一个分配记录ID
+                    $firstAllocation = Db::table('ba_company_package_allocation')
+                        ->where('order_item_id', $item['id'])
+                        ->where('record_type', 2)
+                        ->order('seat_index', 'asc')
+                        ->find();
+
+                    if ($firstAllocation) {
+                        Db::table('ba_order_item')
+                            ->where('id', $item['id'])
+                            ->update(['allocation_id' => $firstAllocation['id']]);
+                    }
+                } else {
+                    // 次卡/积分卡使用购买者分配记录ID
+                    Db::table('ba_order_item')
+                        ->where('id', $item['id'])
+                        ->update(['allocation_id' => $buyerAllocationId]);
+                }
+            }
+
+            Db::commit();
+            return true;
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('处理企业套餐购买失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+
+    /**
+     * 获取可分配的企业套餐列表(区分类型)
+     */
+    public function getAvailableCompanyPackages($companyId, $operatorId)
+    {
+        try {
+            // 获取用户角色(创建者或成员)
+            $isCreator = Db::table('ba_company')
+                    ->where('id', $companyId)
+                    ->where('creator_id', $operatorId)
+                    ->count() > 0;
+
+            // 获取企业所有套餐席位
+            $seats = Db::table('ba_company_package_seats')
+                ->alias('cs')
+                ->leftJoin('ba_package_price pp', 'pp.id = cs.package_price_id')
+                ->leftJoin('ba_product p', 'p.id = cs.product_id')
+                ->where('cs.company_id', $companyId)
+                ->where('cs.available_seats', '>', 0)
+                ->where('cs.status', 1)
+                ->field('cs.*, pp.spec_name, pp.card_type, pp.type as package_type, p.name as product_name, p.type as product_type, p.link,pp.image')
+                ->select();
+
+            // 获取企业所有成员(包括创建者)
+            $allMembers = $this->getAllCompanyMembers($companyId);
+            // 处理数据
+            $result = [];
+            foreach ($seats as $seat) {
+                // 获取该套餐已分配的成员
+                $allocatedMembers = $this->getAllocatedMembersForPackage($companyId, $seat['package_price_id']);
+
+                // 获取该套餐已分配的成员ID列表
+                $allocatedMemberIds = array_column($allocatedMembers, 'user_id');
+
+                // 筛选未分配的成员
+                $unallocatedMembers = [];
+                foreach ($allMembers as $member) {
+                    if (!in_array($member['user_id'], $allocatedMemberIds)) {
+                        $unallocatedMembers[] = $member;
+                    }
+                }
+
+                // 组织返回数据
+                $seatItem = [
+                    'seats_id' => $seat['id'],
+                    'product_id' => $seat['product_id'],
+                    'product_name' => $seat['product_name'],
+                    'product_type' => $seat['product_type'], // 1:网页,2:软件
+                    'product_link' => $seat['link'],
+                    'product_image' => $seat['image'],
+                    'package_price_id' => $seat['package_price_id'],
+                    'spec_name' => $seat['spec_name'],
+                    'card_type' => $seat['card_type'],
+                    'card_type_text' => $this->getCardTypeText($seat['card_type']),
+                    'package_type' => $seat['package_type'], // 1个人,2企业
+                    'total_seats' => $seat['total_seats'],
+                    'assigned_seats' => $seat['assigned_seats'],
+                    'available_seats' => $seat['available_seats'],
+                    // 次卡和积分卡相关
+                    'total_times' => $seat['total_times'],
+                    'remaining_times' => $seat['remaining_times'],
+                    'total_points' => $seat['total_points'],
+                    'remaining_points' => $seat['remaining_points'],
+                    'end_time' => $seat['end_time'],
+                    // 已分配成员信息
+                    'allocated_members' => $allocatedMembers,
+                    // 未分配成员信息
+                    'unallocated_members' => $unallocatedMembers,
+                    // 统计信息
+                    'allocated_count' => count($allocatedMembers),
+                    'unallocated_count' => count($unallocatedMembers),
+                    'total_members' => count($allMembers)
+                ];
+
+                $result[] = $seatItem;
+            }
+
+            return [
+                'packages' => $result,
+                'is_creator' => $isCreator,
+                'total_members' => count($allMembers)
+            ];
+
+        } catch (\Throwable $e) {
+            Log::error('获取可分配企业套餐失败: ' . $e->getMessage());
+            return [
+                'packages' => [],
+                'is_creator' => false,
+                'total_members' => 0
+            ];
+        }
+    }
+
+    /**
+     * 获取企业所有成员(包括创建者)
+     */
+    private function getAllCompanyMembers($companyId)
+    {
+        $members = [];
+
+        // 1. 获取企业创建者
+        $company = Db::table('ba_company')
+            ->where('id', $companyId)
+            ->field('id, name as company_name, creator_id')
+            ->find();
+
+        if ($company && $company['creator_id']) {
+            // 获取创建者用户信息
+            $creator = Db::table('ba_user')
+                ->where('id', $company['creator_id'])
+                ->field('id as user_id, nickname as member_name, avatar, email, mobile')
+                ->find();
+
+            if ($creator) {
+                $creator['is_creator'] = 1;
+                $creator['role'] = 1; // 创建者/管理员
+                $members[] = $creator;
+            }
+        }
+
+        // 2. 获取企业成员表中的成员
+        $companyMembers = Db::table('ba_company_member')
+            ->alias('cm')
+            ->leftJoin('ba_user u', 'u.id = cm.user_id')
+            ->where('cm.company_id', $companyId)
+            ->where('cm.status', 1)
+            ->field('
+            cm.id as member_id, 
+            cm.user_id, 
+            COALESCE(cm.name, u.nickname) as member_name,
+            COALESCE(cm.email, u.email) as email,
+            u.avatar,
+            u.mobile,
+            cm.role
+        ')
+            ->select()
+            ->toArray();
+
+        // 过滤掉重复的成员(创建者可能也在成员表中)
+        foreach ($companyMembers as $member) {
+            // 检查是否已经是创建者
+            $isCreator = false;
+            foreach ($members as $existingMember) {
+                if ($existingMember['user_id'] == $member['user_id']) {
+                    $isCreator = true;
+                    break;
+                }
+            }
+
+            if (!$isCreator) {
+                $member['is_creator'] = 0;
+                $members[] = $member;
+            }
+        }
+
+        return $members;
+    }
+
+    /**
+     * 获取指定套餐已分配的成员
+     */
+    private function getAllocatedMembersForPackage($companyId, $packagePriceId)
+    {
+
+        // 获取套餐信息,判断卡类型
+        $package = Db::table('ba_package_price')
+            ->where('id', $packagePriceId)
+            ->field('card_type')
+            ->find();
+
+        if (!$package) {
+            return [];
+        }
+
+        $cardType = $package['card_type'];
+
+        if ($cardType >= 3) {
+            // 日期卡:从分配记录中获取已分配成员
+            return Db::table('ba_company_package_allocation')
+                ->alias('cpa')
+                ->leftJoin('ba_user u', 'u.id = cpa.assigned_to_user_id')
+                ->leftJoin('ba_company_member cm', 'cm.user_id = cpa.assigned_to_user_id AND cm.company_id = cpa.company_id')
+                ->where('cpa.company_id', $companyId)
+                ->where('cpa.package_price_id', $packagePriceId)
+                ->where('cpa.allocation_status', 1) // 已分配
+                ->whereNotNull('cpa.assigned_to_user_id')
+                ->field('
+                cpa.assigned_to_user_id as user_id,
+                COALESCE(cm.name, u.nickname) as member_name,
+                COALESCE(cm.email, u.email) as email,
+                u.avatar,
+                u.mobile,
+                cpa.allocation_time,
+                cpa.start_time,
+                cpa.end_time,
+                cpa.remaining_times,
+                cpa.remaining_points
+            ')
+                ->select()
+                ->toArray();
+        } else {
+            // 次卡/积分卡:从席位表的授权用户列表中获取
+            $seats = Db::table('ba_company_package_seats')
+                ->where('company_id', $companyId)
+                ->where('package_price_id', $packagePriceId)
+                ->field('authorized_users')
+                ->find();
+
+            if (!$seats || empty($seats['authorized_users'])) {
+                return [];
+            }
+
+            $authorizedUserIds = json_decode($seats['authorized_users'], true);
+            if (empty($authorizedUserIds)) {
+                return [];
+            }
+
+            // 获取授权用户的详细信息
+
+           $user=Db::table('ba_user')
+            ->alias('u')
+            // 使用字符串条件替代闭包
+            ->leftJoin('ba_company_member cm', 'cm.user_id = u.id AND cm.company_id = ' . $companyId)
+            ->whereIn('u.id', $authorizedUserIds)
+            ->field('
+                u.id as user_id,
+                COALESCE(cm.name, u.nickname) as member_name,
+                COALESCE(cm.email, u.email) as email,
+                u.avatar,
+                u.mobile,
+                cm.role
+            ')
+            ->select()
+            ->toArray();
+            return $user;
+        }
+    }
+
+    /**
+     * 分配套餐给企业成员
+     */
+    public function allocatePackage($seatsId, $assignedToUserId, $operatorId, $remark = '')
+    {
+        Db::startTrans();
+        try {
+            // 1. 获取席位信息
+            $seats = Db::table('ba_company_package_seats')
+                ->where('id', $seatsId)
+                ->lock(true)
+                ->find();
+
+            if (!$seats || $seats['available_seats'] <= 0) {
+                throw new \Exception('没有可用席位');
+            }
+            // 2. 检查是否是日期卡
+            if ($seats['card_type'] < 3) {
+                throw new \Exception('非日期卡类型,请使用assignPackagePermission方法');
+            }
+            // 3. 检查权限(只有创建者可以分配其他人的,成员只能分配自己的)
+            $isCreator = Db::table('ba_company')
+                    ->where('id', $seats['company_id'])
+                    ->where('creator_id', $operatorId)
+                    ->count() > 0;
+            // 4. 获取一个未分配的购买记录
+            $availableAllocation = Db::table('ba_company_package_allocation')
+                ->where('company_id', $seats['company_id'])
+                ->where('package_price_id', $seats['package_price_id'])
+                ->where('record_type', 2) // 购买记录
+                ->where('allocation_status', 0) // 未分配
+                ->where('assigned_to_user_id', null) // 未指定用户
+                ->lock(true)
+                ->order('seat_index', 'asc') // 按席位索引排序
+                ->find();
+            if (!$availableAllocation) {
+                throw new \Exception('没有可分配的套餐记录');
+            }
+
+            // 如果不是创建者,检查是否是购买者
+            if (!$isCreator && $availableAllocation['buyer_id'] != $operatorId) {
+                throw new \Exception('您只能分配自己购买的套餐');
+            }
+
+            // 5. 检查成员是否属于该企业
+            $isMember = Db::table('ba_company_member')
+                    ->where('company_id', $seats['company_id'])
+                    ->where('user_id', $assignedToUserId)
+                    ->where('status', 1)
+                    ->count() > 0;
+
+            if (!$isMember) {
+                throw new \Exception('该用户不是企业成员');
+            }
+
+            // 6. 检查该成员是否已分配此套餐(日期卡只能分配给一个成员)
+            if ($seats['card_type'] >= 3) { // 日期卡
+                $alreadyAllocated = Db::table('ba_company_package_allocation')
+                    ->where('company_id', $seats['company_id'])
+                    ->where('package_price_id', $seats['package_price_id'])
+                    ->where('assigned_to_user_id', $assignedToUserId)
+                    ->where('allocation_status', 1)
+                    ->count();
+
+                if ($alreadyAllocated > 0) {
+                    throw new \Exception('该成员已分配此套餐,如需再次分配请重新购买');
+                }
+            }
+
+            // 7. 执行分配
+            $now = date('Y-m-d H:i:s');
+            // 计算到期时间
+            $endTime = $this->calculateEndTime($seats['card_type'], $now);
+            // 更新分配记录
+            Db::table('ba_company_package_allocation')
+                ->where('id', $availableAllocation['id'])
+                ->update([
+                    'assigned_to_user_id' => $assignedToUserId,
+                    'assigned_by_user_id' => $operatorId,
+                    'allocation_status' => 1,
+                    'allocation_time' => $now,
+                    'start_time' => $now,
+                    'end_time' => $endTime,
+                    'expire_time' => $endTime, // 更新冗余字段
+                    'update_time' => $now
+                ]);
+
+            // 8.更新席位表
+            Db::table('ba_company_package_seats')
+                ->where('id', $seatsId)
+                ->update([
+                    'assigned_seats' => Db::raw('assigned_seats + 1'),
+                    'available_seats' => Db::raw('available_seats - 1'),
+                    'update_time' => $now
+                ]);
+
+            // 9. 检查是否所有席位都已分配
+            $unallocatedCount = Db::table('ba_company_package_allocation')
+                ->where('order_item_id', $availableAllocation['order_item_id'])
+                ->where('allocation_status', 0)
+                ->where('record_type', 2)
+                ->count();
+
+            if ($unallocatedCount == 0) {
+                // 所有席位都已分配,更新订单商品
+                Db::table('ba_order_item')
+                    ->where('id', $availableAllocation['order_item_id'])
+                    ->update([
+                        'is_allocated' => 1,
+                        'company_id' => $seats['company_id']
+                    ]);
+            }
+            // 10. 为用户创建产品使用记录
+            $userProductService = new UserProductService();
+            $userProductService->createCompanyProductUsage($availableAllocation, $assignedToUserId, $seats['card_type']);
+
+            // 11. 记录分配日志
+            Db::table('ba_allocation_log')->insert([
+                'allocation_id' => $availableAllocation['id'],
+                'action' => 1, // 分配
+                'operator_id' => $operatorId,
+                'to_user_id' => $assignedToUserId,
+                'remark' => $remark,
+                'create_time' => $now
+            ]);
+
+            Db::commit();
+            return [
+                'success' => true,
+                'allocation_id' => $availableAllocation['id'],
+                'message' => '分配成功'
+            ];
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('分配套餐失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * 取消分配(日期卡独占席位模式)
+     */
+    public function cancelAllocation($allocationId, $operatorId, $remark = '')
+    {
+        Db::startTrans();
+        try {
+            // 1. 获取分配记录
+            $allocation = Db::table('ba_company_package_allocation')
+                ->where('id', $allocationId)
+                ->where('record_type', 2) // 购买记录
+                ->where('allocation_status', 1) // 已分配
+                ->lock(true)
+                ->find();
+
+            if (!$allocation) {
+                throw new \Exception('分配记录不存在或未分配');
+            }
+
+            // 2. 检查权限
+            $isCreator = Db::table('ba_company')
+                    ->where('id', $allocation['company_id'])
+                    ->where('creator_id', $operatorId)
+                    ->count() > 0;
+
+            if (!$isCreator && $allocation['assigned_by_user_id'] != $operatorId) {
+                throw new \Exception('您没有取消分配的权限');
+            }
+
+            // 3. 获取席位信息
+            $seats = Db::table('ba_company_package_seats')
+                ->where('company_id', $allocation['company_id'])
+                ->where('package_price_id', $allocation['package_price_id'])
+                ->find();
+
+            if (!$seats) {
+                throw new \Exception('企业套餐席位不存在');
+            }
+            // 4. 执行取消分配
+            $now = date('Y-m-d H:i:s');
+            // 更新分配记录状态
+            Db::table('ba_company_package_allocation')
+                ->where('id', $allocationId)
+                ->update([
+                    'assigned_to_user_id' => null,
+                    'allocation_status' => 2,
+                    'cancel_allocation_time' => $now,
+                    'update_time' => $now
+                ]);
+
+            // 5. 更新席位表
+            Db::table('ba_company_package_seats')
+                ->where('id', $seats['id'])
+                ->update([
+                    'assigned_seats' => Db::raw('assigned_seats - 1'),
+                    'available_seats' => Db::raw('available_seats + 1'),
+                    'update_time' => $now
+                ]);
+
+            // 6. 检查是否还有已分配的记录
+            $allocatedCount = Db::table('ba_company_package_allocation')
+                ->where('order_item_id', $allocation['order_item_id'])
+                ->where('allocation_status', 1)
+                ->count();
+
+            if ($allocatedCount == 0) {
+                // 没有已分配的记录,更新订单商品
+                Db::table('ba_order_item')
+                    ->where('id', $allocation['order_item_id'])
+                    ->update([
+                        'is_allocated' => 0,
+                        'allocation_id' => null
+                    ]);
+            }
+
+            // 7. 删除对应的用户产品使用记录
+            Db::table('ba_user_product_usage')
+                ->where('user_id', $allocation['assigned_to_user_id'])
+                ->where('package_price_id', $allocation['package_price_id'])
+                ->where('company_id', $allocation['company_id'])
+                ->where('package_type', 2) // 企业套餐
+                ->delete();
+
+            // 8. 记录取消分配日志
+            Db::table('ba_allocation_log')->insert([
+                'allocation_id' => $allocationId,
+                'action' => 2, // 取消分配
+                'operator_id' => $operatorId,
+                'from_user_id' => $allocation['assigned_to_user_id'],
+                'remark' => $remark,
+                'create_time' => $now
+            ]);
+
+            Db::commit();
+            return true;
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('取消分配失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * 分配次卡/积分卡使用权限给成员(授权访问共享资源池)
+     */
+    public function assignPackagePermission($seatsId, $memberUserId, $allocatorId, $remark = '')
+    {
+        Db::startTrans();
+        try {
+            // 1. 获取席位信息
+            $seats = Db::table('ba_company_package_seats')
+                ->where('id', $seatsId)
+                ->lock(true)
+                ->find();
+
+            if (!$seats) {
+                throw new \Exception('企业套餐不存在');
+            }
+
+            // 2. 检查权限(只有企业创建者或购买者可以分配)
+            $canAssign = $this->checkAssignPermission($allocatorId, $seats['company_id'], $seatsId);
+            if (!$canAssign) {
+                throw new \Exception('没有分配权限');
+            }
+
+            // 3. 检查成员是否属于企业
+            if (!$this->isCompanyMember($memberUserId, $seats['company_id'])) {
+                throw new \Exception('该用户不是企业成员');
+            }
+
+            // 4. 检查是否已经是次卡或积分卡
+            if (!in_array($seats['card_type'], [1, 2])) {
+                throw new \Exception('只有次卡和积分卡支持权限分配');
+            }
+
+            // 5. 检查该成员是否已被授权
+            $authorizedUsers = json_decode($seats['authorized_users'] ?? '[]', true);
+
+            if (in_array($memberUserId, $authorizedUsers)) {
+                throw new \Exception('该成员已被授权使用此套餐');
+            }
+
+            // 6. 查找通用记录
+            $generalRecord = Db::table('ba_company_package_allocation')
+                ->where('company_id', $seats['company_id'])
+                ->where('package_price_id', $seats['package_price_id'])
+                ->where('record_type', 0) // 通用记录
+                ->where('is_general', 1) // 是通用记录
+                ->order('id', 'asc')
+                ->find();
+            if (!$generalRecord) {
+                throw new \Exception('未找到套餐购买记录');
+            }
+            // 7. 将成员添加到授权列表
+            $authorizedUsers[] = $memberUserId;
+
+            Db::table('ba_company_package_seats')
+                ->where('id', $seatsId)
+                ->update([
+                    'authorized_users' => json_encode($authorizedUsers),
+                    'assigned_member_count' => Db::raw('assigned_member_count + 1'),
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            // 8. 创建新的分配记录
+            $now = date('Y-m-d H:i:s');
+            $newAllocationData = [
+                'order_item_id' => $generalRecord['order_item_id'],
+                'order_id' => $generalRecord['order_id'],
+                'package_price_id' => $seats['package_price_id'],
+                'product_id' => $seats['product_id'],
+                'buyer_id' => $generalRecord['buyer_id'],
+                'company_id' => $seats['company_id'],
+                'assigned_to_user_id' => $memberUserId,
+                'assigned_by_user_id' => $allocatorId,
+                'record_type' => 1, // 用户分配记录
+                'is_general' => 0,
+                'seat_index' => 0,
+                'parent_id' => $generalRecord['id'], // 关联通用记录
+                'allocation_status' => 1, // 已分配
+                'allocation_time' => $now,
+                'start_time' => $now,
+                'end_time' => null,
+                'expire_time' => null,
+                'remaining_times' => $seats['remaining_times'],
+                'remaining_points' => $seats['remaining_points'],
+                'is_used' => 0,
+                'create_time' => $now,
+                'update_time' => $now
+            ];
+
+            $allocationId = Db::table('ba_company_package_allocation')->insertGetId($newAllocationData);
+
+            // 9. 为用户创建产品使用记录
+            $userProductService = new UserProductService();
+            $orderItem = Db::table('ba_order_item')->where('id', $generalRecord['order_item_id'])->find();
+
+            if ($orderItem) {
+                $package = Db::table('ba_package_price')->where('id', $seats['package_price_id'])->find();
+                if ($package) {
+                    $userProductService->createUserProductRecord($memberUserId, $orderItem, $package);
+                }
+            }
+            // 10. 记录分配日志
+            Db::table('ba_allocation_log')->insert([
+                'allocation_id' => $allocationId,
+                'action' => 1, // 分配
+                'operator_id' => $allocatorId,
+                'to_user_id' => $memberUserId,
+                'remark' => $remark ?: '分配次卡/积分卡使用权限',
+                'create_time' => $now
+            ]);
+
+
+            Db::commit();
+
+            return [
+                'success' => true,
+                'allocation_id' => $allocationId,
+                'message' => '分配成功'
+            ];
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            throw $e;
+        }
+    }
+
+    /**
+     * 检查分配权限
+     */
+    private function checkAssignPermission($userId, $companyId, $seatsId = null)
+    {
+        // 1. 检查是否是公司创建者
+        $isCreator = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->where('creator_id', $userId)
+                ->count() > 0;
+
+        if ($isCreator) {
+            return true;
+        }
+
+        // 2. 检查是否是购买者(对于该套餐)
+        if ($seatsId) {
+            $isBuyer = Db::table('ba_company_package_allocation')
+                    ->where('seats_id', $seatsId)
+                    ->where('buyer_id', $userId)
+                    ->count() > 0;
+
+            if ($isBuyer) {
+                return true;
+            }
+        }
+
+        // 3. 检查是否是管理员(根据企业成员表)
+        $isAdmin = Db::table('ba_company_member')
+                ->where('company_id', $companyId)
+                ->where('user_id', $userId)
+                ->where('role', 1) // 1=管理员
+                ->where('status', 1)
+                ->count() > 0;
+
+        return $isAdmin;
+    }
+
+    /**
+     * 检查用户是否是企业成员
+     */
+    private function isCompanyMember($userId, $companyId)
+    {
+        // 检查是否是创建者
+        $isCreator = Db::table('ba_company')
+                ->where('id', $companyId)
+                ->where('creator_id', $userId)
+                ->count() > 0;
+
+        if ($isCreator) {
+            return true;
+        }
+
+        // 检查是否是成员
+        $isMember = Db::table('ba_company_member')
+                ->where('company_id', $companyId)
+                ->where('user_id', $userId)
+                ->where('status', 1)
+                ->count() > 0;
+
+        return $isMember;
+    }
+    /**
+     * 自动分配席位给购买者
+     */
+    public function autoAllocateToBuyer($buyerId, $companyId, $packagePriceId, $orderId)
+    {
+        try {
+            Log::info('开始自动分配席位给购买者', [
+                'buyer_id' => $buyerId,
+                'company_id' => $companyId,
+                'package_price_id' => $packagePriceId,
+                'order_id' => $orderId
+            ]);
+            // 1. 检查购买者是否已分配此套餐
+            $alreadyAllocated = Db::table('ba_company_package_allocation')
+                ->where('company_id', $companyId)
+                ->where('package_price_id', $packagePriceId)
+                ->where('assigned_to_user_id', $buyerId)
+                ->where('allocation_status', 1) // 已分配
+                ->count();
+            Log::info('检查购买者是否已分配此套餐结果', [
+                'already_allocated' => $alreadyAllocated
+            ]);
+            if ($alreadyAllocated > 0) {
+                Log::info('购买者已分配此套餐,跳过自动分配', [
+                    'buyer_id' => $buyerId,
+                    'company_id' => $companyId,
+                    'package_price_id' => $packagePriceId
+                ]);
+                return false; // 已分配,跳过
+            }
+
+            // 2. 获取可用的席位ID(从席位表中获取)
+            $seats = Db::table('ba_company_package_seats')
+                ->where('company_id', $companyId)
+                ->where('package_price_id', $packagePriceId)
+                ->where('available_seats', '>', 0)
+                ->find();
+            Log::info('查询可用席位结果', [
+                'seats' => $seats ? $seats['id'] : null,
+                'available_seats' => $seats ? $seats['available_seats'] : 0
+            ]);
+            if (!$seats) {
+                Log::error('没有可用席位进行自动分配', [
+                    'company_id' => $companyId,
+                    'package_price_id' => $packagePriceId
+                ]);
+                return false;
+            }
+
+            // 3. 获取一个未分配的分配记录
+            $allocation = Db::table('ba_company_package_allocation')
+                ->where('order_id', $orderId)
+                ->where('company_id', $companyId)
+                ->where('package_price_id', $packagePriceId)
+                ->where('allocation_status', 0) // 未分配
+                ->order('id', 'asc')
+                ->find();
+            Log::info('查询未分配记录结果', [
+                'allocation' => $allocation ? $allocation['id'] : null,
+                'order_id' => $orderId
+            ]);
+
+            if (!$allocation) {
+                // 尝试查找任何属于该订单的未分配记录
+                $allocation = Db::table('ba_company_package_allocation')
+                    ->where('order_id', $orderId)
+                    ->where('allocation_status', 0)
+                    ->order('id', 'asc')
+                    ->find();
+
+                Log::info('二次查询未分配记录结果', [
+                    'allocation' => $allocation ? $allocation['id'] : null
+                ]);
+
+                if (!$allocation) {
+                    Log::error('没有找到未分配的分配记录', [
+                        'order_id' => $orderId,
+                        'company_id' => $companyId,
+                        'package_price_id' => $packagePriceId
+                    ]);
+                    return false;
+                }
+            }
+
+            // 4. 获取套餐信息(用于卡类型判断)
+            $package = Db::table('ba_package_price')
+                ->where('id', $packagePriceId)
+                ->find();
+
+            if (!$package) {
+                Log::error('套餐信息不存在', ['package_price_id' => $packagePriceId]);
+                return false;
+            }
+
+            Db::startTrans();
+            try {
+                // 5. 执行分配
+                $now = date('Y-m-d H:i:s');
+
+                // 更新分配记录
+                Db::table('ba_company_package_allocation')
+                    ->where('id', $allocation['id'])
+                    ->update([
+                        'assigned_to_user_id' => $buyerId,
+                        'assigned_by_user_id' => $buyerId,
+                        'allocation_status' => 1,
+                        'allocation_time' => $now,
+                        'start_time' => $now,
+                        'end_time' => $this->calculateEndTime($package['card_type'], $now),
+                        'update_time' => $now
+                    ]);
+
+                // 更新席位表
+                Db::table('ba_company_package_seats')
+                    ->where('id', $seats['id'])
+                    ->update([
+                        'assigned_seats' => Db::raw('assigned_seats + 1'),
+                        'available_seats' => Db::raw('available_seats - 1'),
+                        'update_time' => $now
+                    ]);
+
+                // 更新订单商品
+                Db::table('ba_order_item')
+                    ->where('id', $allocation['order_item_id'])
+                    ->update([
+                        'is_allocated' => 1,
+                        'company_id' => $companyId
+                    ]);
+
+                // 6. 记录分配日志
+                Db::table('ba_allocation_log')->insert([
+                    'allocation_id' => $allocation['id'],
+                    'action' => 1, // 分配
+                    'operator_id' => $buyerId,
+                    'to_user_id' => $buyerId,
+                    'remark' => '购买后自动分配给购买者',
+                    'create_time' => $now
+                ]);
+
+                // 7. 根据卡类型创建用户产品记录
+                $userProductService = new UserProductService();
+
+                // 所有卡类型都需要创建使用记录
+                if ($package['card_type'] >= 3) { // 日期卡
+                // 日期卡分配时创建使用记录
+                $userProductService->createCompanyProductUsage($allocation, $buyerId, $package['card_type']);
+                } else { // 次卡(1)和积分卡(2)
+                    // 获取订单商品信息
+                    $orderItem = Db::table('ba_order_item')
+                        ->where('id', $allocation['order_item_id'])
+                        ->find();
+
+                    if ($orderItem) {
+                        // 创建用户产品记录(次卡/积分卡)
+                        $userProductService->createUserProductRecord($buyerId, $orderItem, $package);
+                    }
+                }
+
+                Db::commit();
+
+                Log::info('企业套餐自动分配给购买者成功', [
+                    'buyer_id' => $buyerId,
+                    'company_id' => $companyId,
+                    'package_price_id' => $packagePriceId,
+                    'allocation_id' => $allocation['id']
+                ]);
+
+                return true;
+
+            } catch (\Throwable $e) {
+                Db::rollback();
+                Log::error('企业套餐自动分配给购买者失败: ' . $e->getMessage());
+                throw $e;
+            }
+
+        } catch (\Throwable $e) {
+            Log::error('自动分配企业套餐给购买者失败: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 计算到期时间
+     */
+    private function calculateEndTime($cardType, $startTime)
+    {
+        $start = strtotime($startTime);
+
+        switch ($cardType) {
+            case 3: // 日卡
+                return date('Y-m-d 23:59:59', strtotime('+1 day', $start));
+            case 4: // 周卡
+                return date('Y-m-d 23:59:59', strtotime('+7 days', $start));
+            case 5: // 季卡
+                return date('Y-m-d 23:59:59', strtotime('+90 days', $start));
+            case 6: // 月卡
+                return date('Y-m-d 23:59:59', strtotime('+30 days', $start));
+            case 7: // 年卡
+                return date('Y-m-d 23:59:59', strtotime('+365 days', $start));
+            default:
+                return null; // 次卡和积分卡没有固定到期时间
+        }
+    }
+
+    /**
+     * 获取卡类型文本
+     */
+    private function getCardTypeText($type)
+    {
+        $map = [
+            1 => '次卡',
+            2 => '积分卡',
+            3 => '日卡',
+            4 => '周卡',
+            5 => '季卡',
+            6 => '月卡',
+            7 => '年卡'
+        ];
+        return $map[$type] ?? '其他';
+    }
+
+    private function getProductIdByPackage($packagePriceId) {
+        // 实现获取产品ID的逻辑
+        $package = Db::table('ba_package_price')->where('id', $packagePriceId)->find();
+        $product = Db::table('ba_product')->where('package_id', $package['package_id'])->find();
+        return $product ? $product['id'] : 0;
+    }
+
+    private function getOrderBuyerId($orderId) {
+        $order = Db::table('ba_order')->where('id', $orderId)->find();
+        return $order ? $order['user_id'] : 0;
+    }
+
+    /**
+     * 取消次卡/积分卡权限分配
+     */
+    public function cancelPackagePermission($allocationId, $operatorId, $remark = '')
+    {
+        Db::startTrans();
+        try {
+            // 1. 获取分配记录
+            $allocation = Db::table('ba_company_package_allocation')
+                ->where('id', $allocationId)
+                ->where('record_type', 1) // 用户分配记录
+                ->where('allocation_status', 1) // 已分配
+                ->lock(true)
+                ->find();
+
+            if (!$allocation) {
+                throw new \Exception('分配记录不存在或未分配');
+            }
+
+            // 2. 检查权限
+            $canCancel = $this->checkAssignPermission($operatorId, $allocation['company_id']);
+            if (!$canCancel) {
+                throw new \Exception('您没有取消分配的权限');
+            }
+
+            // 3. 获取席位信息
+            $seats = Db::table('ba_company_package_seats')
+                ->where('company_id', $allocation['company_id'])
+                ->where('package_price_id', $allocation['package_price_id'])
+                ->find();
+
+            if (!$seats) {
+                throw new \Exception('企业套餐席位不存在');
+            }
+
+            // 4. 从授权列表中移除用户
+            $authorizedUsers = json_decode($seats['authorized_users'] ?? '[]', true);
+            $key = array_search($allocation['assigned_to_user_id'], $authorizedUsers);
+
+            if ($key !== false) {
+                unset($authorizedUsers[$key]);
+                $authorizedUsers = array_values($authorizedUsers); // 重新索引数组
+
+                // 更新席位表
+                Db::table('ba_company_package_seats')
+                    ->where('id', $seats['id'])
+                    ->update([
+                        'authorized_users' => json_encode($authorizedUsers),
+                        'assigned_member_count' => Db::raw('assigned_member_count - 1'),
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            }
+
+            // 5. 更新分配记录状态
+            $now = date('Y-m-d H:i:s');
+            Db::table('ba_company_package_allocation')
+                ->where('id', $allocationId)
+                ->update([
+                    'allocation_status' => 2, // 已取消
+                    'cancel_allocation_time' => $now,
+                    'update_time' => $now
+                ]);
+            // 6. 删除对应的用户产品使用记录
+            Db::table('ba_user_product_usage')
+                ->where('user_id', $allocation['assigned_to_user_id'])
+                ->where('package_price_id', $allocation['package_price_id'])
+                ->where('company_id', $allocation['company_id'])
+                ->where('package_type', 2) // 企业套餐
+                ->delete();
+
+            // 7. 记录取消分配日志
+            Db::table('ba_allocation_log')->insert([
+                'allocation_id' => $allocationId,
+                'action' => 2, // 取消分配
+                'operator_id' => $operatorId,
+                'from_user_id' => $allocation['assigned_to_user_id'],
+                'remark' => $remark ?: '取消次卡/积分卡使用权限',
+                'create_time' => $now
+            ]);
+
+            Db::commit();
+            return true;
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('取消次卡/积分卡权限失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+    /**
+     * 使用企业次卡/积分卡(从共享资源池扣除)
+     */
+    public function useSharedCompanyPackage($userId, $packagePriceId, $useQuantity, $productId = null, $remark = '')
+    {
+        Db::startTrans();
+        try {
+            // 1. 获取用户所在企业
+            $companyInfo = $this->getUserCompany($userId);
+            if (!$companyInfo) {
+                throw new \Exception('您不属于任何企业');
+            }
+
+            // 2. 获取套餐信息
+            $package = Db::table('ba_package_price')
+                ->where('id', $packagePriceId)
+                ->find();
+
+            if (!$package) {
+                throw new \Exception('套餐不存在');
+            }
+
+            // 3. 检查卡类型是否为次卡或积分卡
+            if (!in_array($package['card_type'], [1, 2])) {
+                throw new \Exception('只有次卡和积分卡支持此使用方式');
+            }
+
+            // 4. 获取企业套餐席位信息,检查用户是否被授权
+            $seats = Db::table('ba_company_package_seats')
+                ->where('company_id', $companyInfo['id'])
+                ->where('package_price_id', $packagePriceId)
+                ->lock(true)
+                ->find();
+
+            if (!$seats) {
+                throw new \Exception('企业套餐不存在');
+            }
+
+            // 检查用户是否在授权列表中
+            $authorizedUsers = json_decode($seats['authorized_users'] ?? '[]', true);
+            if (!in_array($userId, $authorizedUsers)) {
+                throw new \Exception('您未被授权使用此套餐');
+            }
+
+            // 5. 检查共享资源池是否足够
+            if ($package['card_type'] == 1) { // 次卡
+                if ($seats['remaining_times'] < $useQuantity) {
+                    throw new \Exception('剩余次数不足,剩余:' . $seats['remaining_times'] . '次');
+                }
+            } elseif ($package['card_type'] == 2) { // 积分卡
+                if ($seats['remaining_points'] < $useQuantity) {
+                    throw new \Exception('剩余积分不足,剩余:' . $seats['remaining_points'] . '积分');
+                }
+            }
+
+            // 6. 获取用户的分配记录
+            $userAllocation = Db::table('ba_company_package_allocation')
+                ->where('company_id', $companyInfo['id'])
+                ->where('package_price_id', $packagePriceId)
+                ->where('assigned_to_user_id', $userId)
+                ->where('allocation_status', 1)
+                ->order('id', 'asc')
+                ->find();
+
+            if (!$userAllocation) {
+                throw new \Exception('未找到您的分配记录');
+            }
+
+            // 7. 扣减共享池资源
+            if ($package['card_type'] == 1) { // 次卡
+                Db::table('ba_company_package_seats')
+                    ->where('id', $seats['id'])
+                    ->dec('remaining_times', $useQuantity)
+                    ->update();
+
+                $remainingTimesAfter = $seats['remaining_times'] - $useQuantity;
+                $remainingPointsAfter = $seats['remaining_points'];
+            } else { // 积分卡
+                Db::table('ba_company_package_seats')
+                    ->where('id', $seats['id'])
+                    ->dec('remaining_points', $useQuantity)
+                    ->update();
+
+                $remainingTimesAfter = $seats['remaining_times'];
+                $remainingPointsAfter = $seats['remaining_points'] - $useQuantity;
+            }
+
+            // 8. 更新分配记录的使用状态
+            Db::table('ba_company_package_allocation')
+                ->where('id', $userAllocation['id'])
+                ->update([
+                    'is_used' => 1,
+                    'usage_count' => Db::raw('usage_count + ' . $useQuantity),
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            // 9. 记录使用明细
+            $usageData = [
+                'allocation_id' => $userAllocation['id'],
+                'company_id' => $companyInfo['id'],
+                'package_price_id' => $packagePriceId,
+                'user_id' => $userId,
+                'card_type' => $package['card_type'],
+                'used_quantity' => $useQuantity,
+                'remaining_times_after' => $remainingTimesAfter,
+                'remaining_points_after' => $remainingPointsAfter,
+                'product_id' => $productId,
+                'usage_time' => date('Y-m-d H:i:s'),
+                'remark' => $remark,
+                'create_time' => date('Y-m-d H:i:s')
+            ];
+
+            $usageId = Db::table('ba_company_package_usage')->insertGetId($usageData);
+
+            Db::commit();
+
+            return [
+                'success' => true,
+                'usage_id' => $usageId,
+                'remaining_times' => $remainingTimesAfter,
+                'remaining_points' => $remainingPointsAfter,
+                'message' => '使用成功'
+            ];
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('使用企业次卡/积分卡失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+}

+ 342 - 0
app/common/service/InvitationService.php

@@ -0,0 +1,342 @@
+<?php
+namespace app\common\service;
+
+use think\facade\Db;
+use think\facade\Log;
+
+class InvitationService
+{
+    /**
+     * 生成唯一邀请码
+     */
+    public function generateInvitationCode(): string
+    {
+        $maxAttempts = 10;
+        $attempts = 0;
+
+        while ($attempts < $maxAttempts) {
+            // 方法1:使用数字+字母组合
+            $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // 去掉容易混淆的字符
+            $code = '';
+            for ($i = 0; $i < 6; $i++) {
+                $code .= $chars[mt_rand(0, strlen($chars) - 1)];
+            }
+
+            // 方法2:或者使用更短的邀请码
+            // $code = strtoupper(substr(md5(uniqid()), 0, 6));
+
+            // 检查邀请码是否已存在
+            $exists = Db::table('ba_user')
+                ->where('invitation_code', $code)
+                ->count();
+
+            if (!$exists) {
+                return $code;
+            }
+
+            $attempts++;
+
+            // 如果冲突,尝试不同的算法
+            if ($attempts >= 5) {
+                $code = 'INV' . mt_rand(1000, 9999);
+                $exists = Db::table('ba_user')
+                    ->where('invitation_code', $code)
+                    ->count();
+
+                if (!$exists) {
+                    return $code;
+                }
+            }
+        }
+
+        // 如果还是冲突,使用时间戳+随机数
+        return 'INV' . time() . mt_rand(10, 99);
+    }
+
+    /**
+     * 处理用户注册邀请关系(完整版)
+     */
+    public function processRegistration(int $newUserId, string $invitationCode = ''): bool
+    {
+        try {
+            // 1. 为注册用户生成自己的邀请码
+            $userInvitationCode = $this->generateInvitationCode();
+
+            // 2. 将邀请码保存到用户表
+            Db::table('ba_user')
+                ->where('id', $newUserId)
+                ->update([
+                    'invitation_code' => $userInvitationCode,
+                    'update_time' => time()
+                ]);
+            // 3. 处理邀请关系(如果提供了邀请码)
+            if (!empty($invitationCode)) {
+                $this->processInvitationRelationship($newUserId, $invitationCode);
+            }
+
+            // 4. 初始化用户的佣金记录
+            $this->initUserCommission($newUserId);
+
+            Log::info("用户邀请码生成成功: 用户ID={$newUserId}, 邀请码={$userInvitationCode}");
+
+            return true;
+        } catch (\Throwable $e) {
+            Log::error("处理用户注册邀请关系失败: " . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 处理邀请关系(新用户使用别人的邀请码注册)
+     */
+    private function processInvitationRelationship(int $newUserId, string $invitationCode): void
+    {
+        try {
+            // 查找邀请人(通过邀请码)
+            $inviter = Db::table('ba_user')
+                ->where('invitation_code', $invitationCode)
+                ->field('id, username')
+                ->find();
+
+            if (!$inviter) {
+                Log::warning("邀请码不存在: {$invitationCode}");
+                return;
+            }
+
+            // 检查是否已存在邀请关系
+            $exists = Db::table('ba_user_invitation')
+                ->where('invitee_id', $newUserId)
+                ->find();
+
+            if ($exists) {
+                return; // 已有邀请关系,直接返回
+            }
+
+            // 创建邀请关系记录
+            Db::table('ba_user_invitation')->insert([
+                'inviter_id' => $inviter['id'],
+                'invitee_id' => $newUserId,
+                'invitation_code' => $invitationCode,
+                'create_time' => date('Y-m-d H:i:s')
+            ]);
+
+            // 更新邀请人统计
+            $this->updateInviterStats($inviter['id']);
+
+            Log::info("邀请关系创建成功: 邀请人ID={$inviter['id']}, 被邀请人ID={$newUserId}");
+
+        } catch (\Throwable $e) {
+            Log::error("处理邀请关系失败: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * 更新邀请人统计
+     */
+    private function updateInviterStats(int $inviterId): void
+    {
+        try {
+            // 统计邀请人数
+            $invitationCount = Db::table('ba_user_invitation')
+                ->where('inviter_id', $inviterId)
+                ->count();
+
+            // 获取当前佣金记录
+            $commission = Db::table('ba_user_commission')
+                ->where('user_id', $inviterId)
+                ->find();
+
+            if ($commission) {
+                // 更新现有记录
+                Db::table('ba_user_commission')
+                    ->where('user_id', $inviterId)
+                    ->update([
+                        'invitation_count' => $invitationCount,
+                        'total_invitation' => $invitationCount,
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            } else {
+                // 创建新记录(如果邀请人还没有佣金记录)
+                $promotionLink = $this->generatePromotionLink($inviterId);
+
+                Db::table('ba_user_commission')->insert([
+                    'user_id' => $inviterId,
+                    'total_commission' => 0.00,
+                    'current_commission' => 0.00,
+                    'withdrawn_commission' => 0.00,
+                    'frozen_commission' => 0.00,
+                    'promotion_link' => $promotionLink,
+                    'invitation_count' => $invitationCount,
+                    'total_invitation' => $invitationCount,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+            }
+        } catch (\Throwable $e) {
+            Log::error("更新邀请人统计失败: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * 初始化用户佣金记录
+     */
+    private function initUserCommission(int $userId): void
+    {
+        try {
+            $exists = Db::table('ba_user_commission')
+                ->where('user_id', $userId)
+                ->find();
+
+            if (!$exists) {
+                $promotionLink = $this->generatePromotionLink($userId);
+
+                Db::table('ba_user_commission')->insert([
+                    'user_id' => $userId,
+                    'total_commission' => 0.00,
+                    'current_commission' => 0.00,
+                    'withdrawn_commission' => 0.00,
+                    'frozen_commission' => 0.00,
+                    'promotion_link' => $promotionLink,
+                    'invitation_count' => 0,
+                    'total_invitation' => 0,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+            }
+        } catch (\Throwable $e) {
+            Log::error("初始化用户佣金失败: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * 生成推广链接
+     */
+    private function generatePromotionLink(int $userId): string
+    {
+        // 获取用户的邀请码
+        $user = Db::table('ba_user')
+            ->where('id', $userId)
+            ->field('invitation_code')
+            ->find();
+
+        $invitationCode = $user['invitation_code'] ?? '';
+
+        if (empty($invitationCode)) {
+            // 如果用户还没有邀请码,先生成一个
+            $invitationCode = $this->generateInvitationCode();
+            Db::table('ba_user')
+                ->where('id', $userId)
+                ->update(['invitation_code' => $invitationCode]);
+        }
+
+        $domain = request()->domain();
+        return $domain . '/#/user/register?invite=' . $invitationCode;
+    }
+
+    /**
+     * 获取用户邀请统计
+     */
+    public function getUserInvitationStats(int $userId): array
+    {
+        try {
+            // 获取用户的邀请码
+            $user = Db::table('ba_user')
+                ->where('id', $userId)
+                ->field('invitation_code')
+                ->find();
+
+            // 获取邀请关系
+            $invitations = Db::table('ba_user_invitation')
+                ->alias('ui')
+                ->leftJoin('ba_user u', 'u.id = ui.invitee_id')
+                ->where('ui.inviter_id', $userId)
+                ->field('u.id, u.username, u.email, u.mobile, u.create_time, ui.create_time as invite_time')
+                ->order('ui.create_time', 'desc')
+                ->select()
+                ->toArray();
+
+            // 获取佣金信息
+            $commission = Db::table('ba_user_commission')
+                ->where('user_id', $userId)
+                ->find();
+
+            // 获取佣金流水
+            $flows = Db::table('ba_commission_flow')
+                ->where('user_id', $userId)
+                ->order('create_time', 'desc')
+                ->limit(20)
+                ->select()
+                ->toArray();
+
+            return [
+                'invitation_code' => $user['invitation_code'] ?? '', // 用户的邀请码
+                'invitations' => $invitations,
+                'invitation_count' => count($invitations),
+                'commission' => $commission ?: [
+                    'total_commission' => 0,
+                    'current_commission' => 0,
+                    'withdrawn_commission' => 0,
+                    'frozen_commission' => 0,
+                    'promotion_link' => $this->generatePromotionLink($userId),
+                    'invitation_count' => 0,
+                    'total_invitation' => 0
+                ],
+                'flows' => $flows
+            ];
+        } catch (\Throwable $e) {
+            Log::error("获取用户邀请统计失败: " . $e->getMessage());
+            return [
+                'invitation_code' => '',
+                'invitations' => [],
+                'invitation_count' => 0,
+                'commission' => [
+                    'total_commission' => 0,
+                    'current_commission' => 0,
+                    'withdrawn_commission' => 0,
+                    'frozen_commission' => 0,
+                    'promotion_link' => $this->generatePromotionLink($userId),
+                    'invitation_count' => 0,
+                    'total_invitation' => 0
+                ],
+                'flows' => []
+            ];
+        }
+    }
+
+    /**
+     * 获取下级用户列表
+     */
+    public function getSubordinateList(int $userId, int $page = 1, int $pageSize = 10): array
+    {
+        try {
+            $query = Db::table('ba_user_invitation')
+                ->alias('ui')
+                ->leftJoin('ba_user u', 'u.id = ui.invitee_id')
+                ->where('ui.inviter_id', $userId);
+
+            $total = $query->count();
+
+            $list = $query->field('u.id, u.username, u.nickname, u.email, u.mobile, u.create_time, 
+                    u.last_login_time, u.status, ui.create_time as invite_time')
+                ->page($page, $pageSize)
+                ->order('ui.create_time', 'desc')
+                ->select()
+                ->toArray();
+
+            return [
+                'list' => $list,
+                'total' => $total,
+                'page' => $page,
+                'page_size' => $pageSize,
+                'page_count' => ceil($total / $pageSize)
+            ];
+        } catch (\Throwable $e) {
+            Log::error("获取下级用户列表失败: " . $e->getMessage());
+            return [
+                'list' => [],
+                'total' => 0,
+                'page' => $page,
+                'page_size' => $pageSize,
+                'page_count' => 0
+            ];
+        }
+    }
+}

+ 21 - 0
app/common/service/PaymentFactory.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\common\service;
+
+class PaymentFactory
+{
+    /**
+     * 创建支付服务实例
+     */
+    public static function create($payMethod, $payConfigId = 0)
+    {
+        switch ($payMethod) {
+            case 'wechat':
+                return new WechatPaymentService($payMethod, $payConfigId);
+            case 'alipay':
+                return new AlipayPaymentService($payMethod, $payConfigId);
+            default:
+                throw new \Exception('不支持的支付方式');
+        }
+    }
+}

+ 390 - 0
app/common/service/UserProductService.php

@@ -0,0 +1,390 @@
+<?php
+namespace app\common\service;
+
+use think\facade\Db;
+use think\facade\Log;
+
+class UserProductService
+{
+    /**
+     * 创建用户产品使用记录(支付成功后调用)
+     */
+    public function createUserProductRecord($userId, $orderItem, $package)
+    {
+        Db::startTrans();
+        try {
+            // 1. 首先检查是否已存在相同的记录
+            $existing = Db::table('ba_user_product_usage')
+                ->where('order_item_id', $orderItem['id'])
+                ->where('user_id', $userId)
+                ->lock(true)
+                ->find();
+
+            if ($existing) {
+                Db::commit();
+                Log::info('用户产品记录已存在,跳过创建', [
+                    'order_item_id' => $orderItem['id'],
+                    'user_id' => $userId,
+                    'existing_id' => $existing['id']
+                ]);
+                return $existing['id'];
+            }
+
+            // 2. 创建使用记录(原有逻辑)
+            $usageData = [
+                'user_id' => $userId,
+                'order_id' => $orderItem['order_id'],
+                'order_item_id' => $orderItem['id'],
+                'product_id' => $orderItem['product_id'],
+                'package_price_id' => $orderItem['package_price_id'],
+                'package_type' => $package['type'],
+                'card_type' => $package['card_type'],
+                'start_time' => date('Y-m-d H:i:s'),
+                'status' => 1,
+                'create_time' => date('Y-m-d H:i:s'),
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            // 3. 根据卡类型和购买数量计算初始值
+            $quantity = $orderItem['quantity'] ?? 1;
+            switch ($package['card_type']) {
+                case 1: // 次卡
+                    $unitValue = $package['unit_value'] ?? 0;
+                    $usageData['remaining_times'] = $unitValue * $quantity;
+                    break;
+                case 2: // 积分卡
+                    $pointsValue = $package['points_value'] ?? 0;
+                    $usageData['remaining_points'] = $pointsValue * $quantity;
+                    break;
+                case 3: // 日卡
+                    $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+1 day'));
+                    break;
+                case 4: // 周卡
+                    $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+7 days'));
+                    break;
+                case 5: // 季卡
+                    $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+90 days'));
+                    break;
+                case 6: // 月卡
+                    $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+30 days'));
+                    break;
+                case 7: // 年卡
+                    $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+365 days'));
+                    break;
+            }
+
+            $usageId = Db::table('ba_user_product_usage')->insertGetId($usageData);
+
+            // 4. 如果是个人套餐,同时更新ba_user_product表
+            if ($package['type'] == 1) {
+                $this->updateUserProductTable($userId, $orderItem, $package, $usageData);
+            }
+
+            Db::commit();
+            Log::info('用户产品记录创建成功', [
+                'usage_id' => $usageId,
+                'user_id' => $userId,
+                'order_item_id' => $orderItem['id']
+            ]);
+
+            return $usageId;
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('创建用户产品记录失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * 更新用户产品表(兼容原有表)
+     */
+    private function updateUserProductTable($userId, $orderItem, $package, $usageData)
+    {
+        try {
+        // 检查是否已有记录
+        $userProduct = Db::table('ba_user_product')
+            ->where('user_id', $userId)
+            ->where('product_id', $orderItem['product_id'])
+            ->where('package_price_id', $orderItem['package_price_id'])
+            ->find();
+
+        if ($userProduct) {
+            // 更新现有记录
+            $updateData = [
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            // 根据卡类型累加
+            if ($package['card_type'] == 1) { // 次卡
+                $updateData['remaining_times'] = Db::raw('remaining_times + ' . $usageData['remaining_times']);
+            } elseif ($package['card_type'] == 2) { // 积分卡
+                $updateData['remaining_points'] = Db::raw('remaining_points + ' . $usageData['remaining_points']);
+            } elseif ($package['card_type'] >= 3) { // 日期卡
+                // 日期卡取最新的结束时间
+                if (empty($userProduct['expire_time']) ||
+                    strtotime($usageData['end_time']) > strtotime($userProduct['expire_time'])) {
+                    $updateData['expire_time'] = $usageData['end_time'];
+                }
+            }
+
+            Db::table('ba_user_product')
+                ->where('id', $userProduct['id'])
+                ->update($updateData);
+        } else {
+            // 创建新记录
+            $newData = [
+                'user_id' => $userId,
+                'product_id' => $orderItem['product_id'],
+                'package_price_id' => $orderItem['package_price_id'],
+                'remaining_points' => $usageData['remaining_points'] ?? 0,
+                'remaining_times' => $usageData['remaining_times'] ?? 0,
+                'expire_time' => $usageData['end_time'] ?? null,
+                'create_time' => date('Y-m-d H:i:s')
+            ];
+
+            Db::table('ba_user_product')->insert($newData);
+        }
+        } catch (\Throwable $e) {
+            Log::error('更新用户产品表失败: ' . $e->getMessage());
+            // 这里不抛出异常,因为这是辅助功能,主功能不受影响
+        }
+    }
+
+    /**
+     * 企业套餐分配时创建用户产品记录
+     */
+    public function createCompanyProductUsage($allocationRecord, $assignedToUserId,$card_type)
+    {
+
+        try {
+        // 从分配记录创建用户产品使用记录
+        $usageData = [
+            'user_id' => $assignedToUserId,
+            'order_id' => $allocationRecord['order_id'],
+            'order_item_id' => $allocationRecord['order_item_id'],
+            'product_id' => $allocationRecord['product_id'],
+            'package_price_id' => $allocationRecord['package_price_id'],
+            'package_type' => 2, // 企业套餐
+            'card_type' => $card_type,
+            'company_id' => $allocationRecord['company_id'],
+            'allocation_id' => $allocationRecord['id'],
+            'is_allocated' => 1,
+            'start_time' => date('Y-m-d H:i:s'),
+            'status' => 1,
+            'create_time' => date('Y-m-d H:i:s'),
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        // 设置有效期和剩余量
+        if ($card_type >= 3) { // 日期卡
+            $usageData['end_time'] = $this->calculateEndTime($card_type);
+        } elseif ($card_type == 1) { // 次卡
+            $usageData['remaining_times'] = $allocationRecord['remaining_times'];
+        } elseif ($card_type == 2) { // 积分卡
+            $usageData['remaining_points'] = $allocationRecord['remaining_points'];
+        }
+
+        return Db::table('ba_user_product_usage')->insertGetId($usageData);
+        } catch (\Throwable $e) {
+            Log::error('创建企业产品使用记录失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * 获取我的产品列表(整合个人套餐和企业套餐)
+     */
+    public function getUserProducts($userId)
+    {
+        try {
+            // 1. 查询个人套餐(直接从使用记录表)
+            $personalProducts = Db::table('ba_user_product_usage')
+                ->alias('upu')
+                ->leftJoin('ba_product p', 'p.id = upu.product_id')
+                ->leftJoin('ba_package_price pp', 'pp.id = upu.package_price_id')
+                ->where('upu.user_id', $userId)
+                ->where('upu.package_type', 1) // 个人套餐
+                ->where('upu.status', 1) // 有效
+                ->field('upu.*, 
+                    p.name as product_name, p.type as product_type, p.link,
+                    pp.spec_name, pp.type as package_type, pp.card_type,pp.image')
+                ->select()
+                ->toArray();
+            // 2. 查询已分配给用户的企业套餐
+            $companyProducts = Db::table('ba_user_product_usage')
+                ->alias('upu')
+                ->leftJoin('ba_product p', 'p.id = upu.product_id')
+                ->leftJoin('ba_package_price pp', 'pp.id = upu.package_price_id')
+                ->leftJoin('ba_company_package_allocation cpa', 'cpa.id = upu.allocation_id')
+                ->where('upu.user_id', $userId)
+                ->where('upu.package_type', 2) // 企业套餐
+                ->where('upu.status', 1) // 有效
+                ->where('upu.is_allocated', 1) // 已分配
+                ->field('upu.*, 
+                    p.name as product_name, p.type as product_type, p.link,
+                    pp.spec_name, pp.type as package_type, pp.card_type,
+                    cpa.assigned_by_user_id,pp.image')
+                ->select()
+                ->toArray();
+            // 3. 合并结果
+            $allProducts = array_merge($personalProducts, $companyProducts);
+            // 4. 按产品ID和套餐规格ID分组,合并相同产品(只合并次卡和积分卡)
+            $groupedProducts = [];
+            foreach ($allProducts as $product) {
+                $key = $product['product_id'] . '_' . $product['package_price_id'];
+
+                // 日期卡不合并,每次分配单独显示
+                if ($product['card_type'] >= 3) {
+                    $groupedProducts[] = $this->formatProductItem($product, false);
+                    continue;
+                }
+
+                // 次卡和积分卡合并
+                if (!isset($groupedProducts[$key])) {
+                    $groupedProducts[$key] = $this->formatProductItem($product, true);
+                } else {
+                    // 累加剩余次数/积分
+                    $groupedProducts[$key]['remaining_times'] += $product['remaining_times'];
+                    $groupedProducts[$key]['remaining_points'] += $product['remaining_points'];
+                    $groupedProducts[$key]['total_quantity'] += 1;
+                }
+            }
+
+            // 5. 处理合并后的数组
+            $result = [];
+            foreach ($groupedProducts as $product) {
+                $result[] = $product;
+            }
+
+            return $result;
+
+        } catch (\Throwable $e) {
+            Log::error('获取用户产品列表失败: ' . $e->getMessage());
+            return [];
+        }
+    }
+
+    /**
+     * 格式化产品项
+     */
+    private function formatProductItem($product, $isMerged = false)
+    {
+        $item = [
+            'product_id' => $product['product_id'],
+            'product_name' => $product['product_name'],
+            'product_type' => $product['product_type'] ?? 1, // 默认1:网页
+            'product_link' => $product['link'] ?? '',
+            'image' => $product['image'] ?? '',
+            'package_spec' => $product['spec_name'],
+            'package_type' => $product['package_type'],
+            'package_type_text' => $product['package_type'] == 1 ? '个人套餐' : '企业套餐',
+            'card_type' => $product['card_type'],
+            'card_type_text' => $this->getCardTypeText($product['card_type']),
+            'total_quantity' => $isMerged ? 1 : 1,
+            'effective_end_time' => $product['end_time'] ?? null,
+            'remaining_times' => $product['remaining_times'] ?? 0,
+            'remaining_points' => $product['remaining_points'] ?? 0,
+            'display_info' => $this->getDisplayInfo($product),
+            'usage_id' => $product['id'],
+            'start_time' => $product['start_time'] ?? null,
+            'last_use_time' => $product['last_use_time'] ?? null,
+            'status' => $product['status'] ?? 1,
+            'buy_time' => $product['create_time']
+        ];
+        // 企业套餐特有字段
+        if ($product['package_type'] == 2) {
+            $item['is_allocated'] = $product['is_allocated'] ?? 0;
+            $item['company_id'] = $product['company_id'] ?? null;
+            $item['allocation_id'] = $product['allocation_id'] ?? null;
+            // 获取企业套餐的席位信息(如果有)
+            if ($item['company_id']) {
+
+                $seatsInfo = Db::name('company_package_seats')
+                    ->where('company_id', $item['company_id'])
+                    ->where('package_price_id', $product['package_price_id'])
+                    ->find();
+
+                if ($seatsInfo) {
+                    $item['total_seats'] = $seatsInfo['total_seats'];
+                    $item['assigned_seats'] = $seatsInfo['assigned_seats'];
+                    $item['available_seats'] = $seatsInfo['available_seats'];
+                } else {
+                    $item['total_seats'] = 0;
+                    $item['assigned_seats'] = 0;
+                    $item['available_seats'] = 0;
+                }
+            } else {
+                $item['total_seats'] = null;
+                $item['assigned_seats'] = null;
+                $item['available_seats'] = null;
+            }
+        } else {
+            // 个人套餐没有席位信息
+            $item['is_allocated'] = 0;
+            $item['company_id'] = null;
+            $item['allocation_id'] = null;
+            $item['total_seats'] = null;
+            $item['assigned_seats'] = null;
+            $item['available_seats'] = null;
+        }
+        return $item;
+    }
+
+    /**
+     * 获取显示信息
+     */
+    private function getDisplayInfo($product)
+    {
+        if ($product['card_type'] >= 3) { // 日期卡
+            $endTime = $product['end_time'] ?? null;
+            return $endTime ? '到期: ' . date('Y-m-d', strtotime($endTime)) : '永久有效';
+        } elseif ($product['card_type'] == 1) { // 次卡
+            $remainingTimes = $product['remaining_times'] ?? 0;
+            return '剩余次数: ' . $remainingTimes;
+        } elseif ($product['card_type'] == 2) { // 积分卡
+            $remainingPoints = $product['remaining_points'] ?? 0;
+            return '剩余积分: ' . $remainingPoints;
+        }
+        return '';
+    }
+
+    /**
+     * 获取卡类型文本
+     */
+    private function getCardTypeText($type)
+    {
+        $map = [
+            1 => '次卡',
+            2 => '积分卡',
+            3 => '日卡',
+            4 => '周卡',
+            5 => '季卡',
+            6 => '月卡',
+            7 => '年卡'
+        ];
+        return $map[$type] ?? '其他';
+    }
+
+    /**
+     * 计算到期时间
+     */
+    private function calculateEndTime($cardType)
+    {
+        $start = time();
+        switch ($cardType) {
+            case 3: // 日卡
+                return date('Y-m-d 23:59:59', strtotime('+1 day', $start));
+            case 4: // 周卡
+                return date('Y-m-d 23:59:59', strtotime('+7 days', $start));
+            case 5: // 季卡
+                return date('Y-m-d 23:59:59', strtotime('+90 days', $start));
+            case 6: // 月卡
+                return date('Y-m-d 23:59:59', strtotime('+30 days', $start));
+            case 7: // 年卡
+                return date('Y-m-d 23:59:59', strtotime('+365 days', $start));
+            default:
+                return null;
+        }
+    }
+}

+ 418 - 0
app/common/service/WechatPaymentService.php

@@ -0,0 +1,418 @@
+<?php
+
+namespace app\common\service;
+
+class WechatPaymentService extends BasePaymentService
+{
+    // 微信支付API地址
+    protected $apiUrl = 'https://api.mch.weixin.qq.com';
+
+    // 配置参数
+    protected $config = [];
+
+    /**
+     * 初始化微信支付配置
+     */
+    protected function initConfig()
+    {
+        $this->config = [
+            'appid' => $this->payConfig['app_id'],
+            'mch_id' => $this->payConfig['mch_id'],
+            'key' => $this->payConfig['key'],
+            'notify_url' => $this->payConfig['notify_url'] ?: url('/api/invitation/payment/wechatNotify', [], true, true)->build(),
+            'cert_path' => $this->getCertPath('cert'),
+            'key_path' => $this->getCertPath('key'),
+        ];
+
+        if (empty($this->config['appid']) || empty($this->config['mch_id']) || empty($this->config['key'])) {
+            throw new \Exception('微信支付配置不完整');
+        }
+    }
+
+    /**
+     * 获取证书路径
+     */
+    protected function getCertPath($type)
+    {
+        // 可以从数据库或文件系统获取证书
+        // 这里简化处理,实际需要根据业务获取
+        return '';
+    }
+
+    /**
+     * 生成支付二维码
+     */
+    public function createQrCode($orderNo, $amount, $subject, $body = '')
+    {
+        try {
+            // 构建请求参数
+            $params = [
+                'appid' => $this->config['appid'],
+                'mch_id' => $this->config['mch_id'],
+                'nonce_str' => $this->generateNonceStr(),
+                'body' => mb_substr($subject, 0, 127, 'UTF-8'), // 商品描述,128字节限制
+                'out_trade_no' => $orderNo,
+                'total_fee' => intval($amount * 100), // 单位:分
+                'spbill_create_ip' => request()->ip(),
+                'notify_url' => $this->config['notify_url'],
+                'trade_type' => 'NATIVE', // 扫码支付
+                'product_id' => $orderNo,
+            ];
+
+            // 生成签名
+            $params['sign'] = $this->generateSign($params);
+
+            // 调用微信统一下单接口
+            $result = $this->postXmlCurl($this->apiUrl . '/pay/unifiedorder', $this->arrayToXml($params));
+
+            // 解析结果
+            $response = $this->xmlToArray($result);
+            if ($response['return_code'] == 'SUCCESS' && $response['result_code'] == 'SUCCESS') {
+                return [
+                    'qr_code_url' => $response['code_url'],
+                    'qr_code_content' => $response['code_url'],
+                    'prepay_id' => $response['prepay_id'],
+                ];
+            } else {
+                $errorMsg = $response['return_msg'] ?? $response['err_code_des'] ?? '微信支付下单失败';
+                throw new \Exception($errorMsg);
+            }
+
+        } catch (\Throwable $e) {
+            $this->log('生成微信支付二维码失败', [
+                'order_no' => $orderNo,
+                'error' => $e->getMessage()
+            ], 'error');
+            throw $e;
+        }
+    }
+
+    /**
+     * 处理支付回调
+     */
+    public function handleNotify($input)
+    {
+        try {
+            // 解析XML数据
+            $data = $this->xmlToArray($input);
+
+            // 验证签名
+            if (!$this->verifySign($data, $data['sign'])) {
+                throw new \Exception('签名验证失败');
+            }
+
+            // 检查支付状态
+            if ($data['return_code'] != 'SUCCESS' || $data['result_code'] != 'SUCCESS') {
+                throw new \Exception('支付失败: ' . ($data['return_msg'] ?? '未知错误'));
+            }
+
+            return [
+                'order_no' => $data['out_trade_no'],
+                'trade_no' => $data['transaction_id'],
+                'pay_amount' => $data['total_fee'] / 100,
+                'pay_time' => $this->formatWxTime($data['time_end']),
+                'raw_data' => $data,
+            ];
+
+        } catch (\Throwable $e) {
+            $this->log('处理微信支付回调失败', [
+                'input' => $input,
+                'error' => $e->getMessage()
+            ], 'error');
+            throw $e;
+        }
+    }
+
+    /**
+     * 查询支付订单
+     */
+    public function queryOrder($orderNo)
+    {
+        try {
+            $params = [
+                'appid' => $this->config['appid'],
+                'mch_id' => $this->config['mch_id'],
+                'out_trade_no' => $orderNo,
+                'nonce_str' => $this->generateNonceStr(),
+            ];
+
+            $params['sign'] = $this->generateSign($params);
+
+            $result = $this->postXmlCurl($this->apiUrl . '/pay/orderquery', $this->arrayToXml($params));
+            $response = $this->xmlToArray($result);
+
+            if ($response['return_code'] == 'SUCCESS' && $response['result_code'] == 'SUCCESS') {
+                $status = $response['trade_state'];
+                $payStatus = 0; // 待支付
+
+                if ($status == 'SUCCESS') {
+                    $payStatus = 1; // 已支付
+                } elseif (in_array($status, ['CLOSED', 'REVOKED', 'PAYERROR', 'USERPAYING'])) {
+                    $payStatus = 2; // 支付失败
+                }
+
+                return [
+                    'order_no' => $response['out_trade_no'],
+                    'trade_no' => $response['transaction_id'],
+                    'pay_status' => $payStatus,
+                    'pay_amount' => $response['total_fee'] / 100,
+                    'pay_time' => isset($response['time_end']) ? $this->formatWxTime($response['time_end']) : null,
+                    'trade_state' => $status,
+                ];
+            }
+
+            throw new \Exception('查询订单失败: ' . ($response['return_msg'] ?? '未知错误'));
+
+        } catch (\Throwable $e) {
+            $this->log('查询微信支付订单失败', [
+                'order_no' => $orderNo,
+                'error' => $e->getMessage()
+            ], 'error');
+            throw $e;
+        }
+    }
+
+    /**
+     * 验证签名
+     */
+    protected function verifySign($data, $sign)
+    {
+        $localSign = $this->generateSign($data);
+        return $localSign === $sign;
+    }
+
+    /**
+     * 生成签名
+     */
+    protected function generateSign($data)
+    {
+        // 过滤空值和sign参数
+        $filterData = array_filter($data, function($value, $key) {
+            return $value !== '' && $value !== null && $key !== 'sign';
+        }, ARRAY_FILTER_USE_BOTH);
+
+        // 按参数名ASCII码从小到大排序
+        ksort($filterData);
+
+        // 拼接字符串
+        $string = '';
+        foreach ($filterData as $key => $value) {
+            $string .= $key . '=' . $value . '&';
+        }
+        $string .= 'key=' . $this->config['key'];
+
+        // MD5加密并转为大写
+        return strtoupper(md5($string));
+    }
+
+    /**
+     * 生成随机字符串
+     */
+    protected function generateNonceStr($length = 32)
+    {
+        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+        $str = '';
+        for ($i = 0; $i < $length; $i++) {
+            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
+        }
+        return $str;
+    }
+
+    /**
+     * 格式化微信支付时间
+     */
+    protected function formatWxTime($timeStr)
+    {
+        if (empty($timeStr)) return null;
+
+        // 微信时间格式:yyyyMMddHHmmss
+        $year = substr($timeStr, 0, 4);
+        $month = substr($timeStr, 4, 2);
+        $day = substr($timeStr, 6, 2);
+        $hour = substr($timeStr, 8, 2);
+        $minute = substr($timeStr, 10, 2);
+        $second = substr($timeStr, 12, 2);
+
+        return "{$year}-{$month}-{$day} {$hour}:{$minute}:{$second}";
+    }
+
+    /**
+     * 发送XML请求
+     */
+    private function postXmlCurl($url, $xml, $useCert = false, $timeout = 30)
+    {
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
+
+        // 设置SSL
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+
+        // 如果需要证书
+        if ($useCert) {
+            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
+            curl_setopt($ch, CURLOPT_SSLCERT, $this->config['cert_path']);
+            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
+            curl_setopt($ch, CURLOPT_SSLKEY, $this->config['key_path']);
+        }
+
+        $response = curl_exec($ch);
+
+        if (curl_errno($ch)) {
+            curl_close($ch);
+            throw new \Exception('CURL错误: ' . curl_error($ch));
+        }
+
+        curl_close($ch);
+        return $response;
+    }
+
+    /**
+     * 数组转XML
+     */
+    private function arrayToXml($arr)
+    {
+        $xml = '<xml>';
+        foreach ($arr as $key => $val) {
+            if (is_numeric($val)) {
+                $xml .= "<{$key}>{$val}</{$key}>";
+            } else {
+                $xml .= "<{$key}><![CDATA[{$val}]]></{$key}>";
+            }
+        }
+        $xml .= '</xml>';
+        return $xml;
+    }
+
+    /**
+     * XML转数组(兼容PHP 8.0+)
+     */
+    private function xmlToArray($xml)
+    {
+        if (empty($xml)) {
+            return [];
+        }
+
+        try {
+            // 方法1:使用DOMDocument(推荐,兼容性好)
+            $dom = new \DOMDocument();
+
+            // 针对PHP版本的兼容处理
+            if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
+                // PHP 8.0+ 不需要禁用实体加载器,直接加载
+                $dom->loadXML($xml, LIBXML_NOENT | LIBXML_NOCDATA);
+            } else {
+                // PHP 8.0以下需要禁用实体加载器
+                $previous = libxml_disable_entity_loader(true);
+                $dom->loadXML($xml, LIBXML_NOENT | LIBXML_NOCDATA);
+                libxml_disable_entity_loader($previous);
+            }
+
+            // 转换为数组
+            $data = $this->domToArray($dom->documentElement);
+
+            return $data ?: [];
+
+        } catch (\Exception $e) {
+            $this->log('XML解析失败', [
+                'xml' => substr($xml, 0, 500), // 只记录前500字符
+                'error' => $e->getMessage()
+            ], 'error');
+
+            // 备用解析方法
+            return $this->fallbackXmlToArray($xml);
+        }
+    }
+
+    /**
+     * DOMElement转换为数组
+     */
+    private function domToArray($node)
+    {
+        $output = [];
+
+        switch ($node->nodeType) {
+            case XML_CDATA_SECTION_NODE:
+            case XML_TEXT_NODE:
+                $output = trim($node->textContent);
+                break;
+
+            case XML_ELEMENT_NODE:
+                for ($i = 0; $i < $node->childNodes->length; $i++) {
+                    $child = $node->childNodes->item($i);
+                    $value = $this->domToArray($child);
+
+                    if (isset($child->tagName)) {
+                        $tag = $child->tagName;
+
+                        if (!isset($output[$tag])) {
+                            $output[$tag] = [];
+                        }
+
+                        if (is_array($value) && isset($value['@value'])) {
+                            $output[$tag][] = $value['@value'];
+                        } else {
+                            $output[$tag][] = $value;
+                        }
+
+                        // 如果是单一元素,不要数组形式
+                        if (count($output[$tag]) === 1 && !is_array($output[$tag][0])) {
+                            $output[$tag] = $output[$tag][0];
+                        }
+                    } elseif ($value !== '') {
+                        $output = $value;
+                    }
+                }
+
+                if ($node->attributes->length) {
+                    $attributes = [];
+                    foreach ($node->attributes as $attrName => $attrNode) {
+                        $attributes[$attrName] = (string)$attrNode->value;
+                    }
+                    if (!empty($output)) {
+                        $output['@attributes'] = $attributes;
+                    } else {
+                        $output = ['@value' => '', '@attributes' => $attributes];
+                    }
+                }
+                break;
+        }
+
+        // 如果是数组且只有一个元素,直接返回那个元素
+        if (is_array($output) && count($output) == 1 && isset($output[0])) {
+            $output = $output[0];
+        }
+
+        return $output;
+    }
+
+    /**
+     * 备用XML解析方法(原来的方法,带版本检查)
+     */
+    private function fallbackXmlToArray($xml)
+    {
+        try {
+            // 版本检查,PHP 8.0以上不调用已废弃函数
+            if (version_compare(PHP_VERSION, '8.0.0', '<')) {
+                libxml_disable_entity_loader(true);
+            }
+
+            $data = json_decode(json_encode(simplexml_load_string(
+                $xml,
+                'SimpleXMLElement',
+                LIBXML_NOCDATA | LIBXML_NOENT
+            )), true);
+
+            return $data ?: [];
+        } catch (\Exception $e) {
+            $this->log('备用XML解析失败', [
+                'error' => $e->getMessage()
+            ], 'error');
+            return [];
+        }
+    }
+}

+ 2 - 4
web/src/api/backend/userCenter/user.ts

@@ -43,12 +43,10 @@ export function removeSubordinate(data: anyObj) {
 }
 
 // 详情
-export function detail(id: number) {
+export function detail(data:anyObj) {
     return createAxios({
         url: '/admin/userCenter.user/detail',
         method: 'get',
-        params: {
-            id: id,
-        },
+        params: data,
     })
 }

+ 28 - 0
web/src/api/frontend/home.ts

@@ -0,0 +1,28 @@
+import createAxios from '/@/utils/axios'
+
+// 分类列表
+export function productList() {
+    return createAxios({
+        url: '/api/product.Package/product_list',
+        method: 'get',
+    })
+}
+
+// 套餐列表
+export function list(data:anyObj) {
+  return createAxios({
+      url: '/api/product.Package/list',
+      params:data,
+      method: 'get',
+  })
+}
+
+// 获取套餐详情
+export function detail(data:anyObj) {
+  return createAxios({
+      url: '/api/product.Package/detail',
+      params:data,
+      method: 'get',
+  })
+}
+

+ 150 - 0
web/src/api/frontend/me.ts

@@ -0,0 +1,150 @@
+import createAxios from '/@/utils/axios'
+
+//我的主页数据
+export function dashboard() {
+  return createAxios({
+    url: '/api/user/dashboard',
+    method: 'get',
+  })
+}
+
+//用户购买记录
+export function purchaseRecordsSimple(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.order/purchaseRecordsSimple',
+    method: 'get',
+    params: data
+  })
+}
+
+//获取订单详情
+export function orderDetail(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.order/orderDetail',
+    method: 'get',
+    params: data,
+  })
+}
+
+//产品列表
+export function purchasedList() {
+  return createAxios({
+    url: '/api/invitation.product/purchasedList',
+    method: 'get',
+  })
+}
+
+//产品详情
+export function productDetail(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.product/detail',
+    method: 'get',
+    params: data,
+  })
+}
+
+//获取下级用户列表
+export function subordinateList(data: anyObj) {
+  return createAxios({
+    url: '/api/user/subordinateList',
+    method: 'get',
+    params: data,
+  })
+}
+
+//检查当前用户是否已有企业
+export function checkCompany() {
+  return createAxios({
+    url: '/api/invitation.Company/checkCompany',
+    method: 'get',
+  })
+}
+
+//获取我的企业列表
+export function myCompanies(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Company/myCompanies',
+    method: 'get',
+    params: data,
+  })
+}
+
+//获取企业成员列表
+export function members(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Company/members',
+    method: 'get',
+    params: data,
+  })
+}
+
+//创建企业
+export function createCompany(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Company/createCompany',
+    method: 'post',
+    data: data,
+  })
+}
+
+//搜索邮箱用户(用于添加成员时搜索)
+export function searchEmail(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Company/searchEmail',
+    method: 'get',
+    params: data,
+  })
+}
+
+//通过邮箱添加成员
+export function addMember(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Company/addMember',
+    method: 'post',
+    data: data,
+  })
+}
+
+//移除企业成员
+export function removeMember(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Company/removeMember',
+    method: 'post',
+    data: data,
+  })
+}
+
+//更新企业信息
+export function updateCompany(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Company/updateCompany',
+    method: 'post',
+    data: data,
+  })
+}
+
+//获取可分配的企业套餐列表和企业成员
+export function allocationList() {
+  return createAxios({
+    url: '/api/invitation.Allocation/allocationList',
+    method: 'get',
+  })
+}
+
+//分配套餐给企业成员
+export function assign(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Allocation/assign',
+    method: 'post',
+    data: data
+  })
+}
+
+//取消分配
+export function cancel(data: anyObj) {
+  return createAxios({
+    url: '/api/invitation.Allocation/cancel',
+    method: 'post',
+    data: data
+  })
+}

+ 46 - 0
web/src/api/frontend/order.ts

@@ -0,0 +1,46 @@
+import createAxios from '/@/utils/axios'
+
+// 创建购买订单
+// {
+// 	"package_id": "1",
+// 	"quantity": "2" //下单数量
+// }
+export function createOrder(data:anyObj) {
+    return createAxios({
+        url: '/api/product.Purchase/createOrder',
+        method: 'post',
+        data:data,
+    })
+}
+
+// 创建支付二维码
+// {
+// 	"order_id": "13",
+// 	"pay_method": "wechat"
+// }
+export function createQrCode(data:anyObj) {
+  return createAxios({
+      url: '/api/product.Payment/createQrCode',
+      method: 'post',
+      data:data,
+  })
+}
+
+// 查询订单详情
+export function checkOrderStatus(data:anyObj) {
+  return createAxios({
+      url: '/api/invitation.order/orderDetail',
+      method: 'get',
+      params:data,
+  })
+}
+
+// 查询订单详情
+export function check(data:anyObj) {
+  return createAxios({
+      url: '/api/product.OrderStatus/check',
+      method: 'get',
+      params:data,
+  })
+}
+

+ 1 - 1
web/src/router/static/memberCenterBase.ts

@@ -11,7 +11,7 @@ export const memberCenterBaseRoutePath = '/user'
 const memberCenterBaseRoute: RouteRecordRaw = {
     path: memberCenterBaseRoutePath,
     name: 'user',
-    component: () => import('/@/layouts/frontend/user.vue'),
+    component: () => import('/@/views/frontend/Me.vue'),
     // 重定向到 loading 路由
     redirect: memberCenterBaseRoutePath + '/loading',
     meta: {

+ 21 - 4
web/src/views/backend/order/order/index.vue

@@ -85,6 +85,18 @@ const baTable = new baTableClass(
 				prop: 'pay_method', 
 				align: 'center', 
 				minWidth: 100,
+				render: 'tag',
+				effect:'dark',
+				custom: {
+					'银行卡': 'warning',
+					'支付宝': '',
+					'微信': 'success',
+				},
+				replaceValue: {
+					'银行卡': '银行卡',
+					'支付宝': '支付宝',
+					'微信': '微信',
+				},
 			},
 			{ 
 				label: '订单状态', 
@@ -92,10 +104,14 @@ const baTable = new baTableClass(
 				align: 'center', 
 				render: 'tag',
 				custom: {
-					'已支付': 'success',
-					'待支付': 'warning',
-					'已取消': 'info',
-					'已退款': 'danger',
+					'0': 'warning',
+					'1': 'success',
+					'2': 'danger',
+				},
+				replaceValue: {
+					'0': '待支付',
+					'1': '已支付',
+					'2': '支付失败',
 				},
 				minWidth: 100,
 			},
@@ -104,6 +120,7 @@ const baTable = new baTableClass(
 				label: '支付时间', 
 				prop: 'pay_time', 
 				align: 'center',
+				minWidth: 170,
 			},
 			{ 
 				label: '订单日期', 

+ 19 - 10
web/src/views/backend/order/withdrawal/index.vue

@@ -8,8 +8,8 @@
 			<template #operate>
                 <el-table-column prop="operate" align="center" label="操作" min-width="150">
                     <template #default="scope">
-                        <el-button type="primary" size="small" @click="handleApprove(scope.row)" v-auth="'approve'">同意</el-button>
-                        <el-button type="danger" size="small" @click="handleReject(scope.row)" v-auth="'reject'">拒绝</el-button>
+                        <el-button :disabled="scope.row.status !== 0" type="primary" size="small" @click="handleApprove(scope.row)" v-auth="'approve'">同意</el-button>
+                        <el-button :disabled="scope.row.status !== 0" type="danger" size="small" @click="handleReject(scope.row)" v-auth="'reject'">拒绝</el-button>
                     </template>
                 </el-table-column>
             </template>
@@ -75,6 +75,13 @@ const baTable = new baTableClass(
 				prop: 'withdrawal_method', 
 				align: 'center', 
 				minWidth: 120,
+				render: 'tag',
+				effect:'dark',
+				custom: {
+					'银行卡': 'info',
+					'微信': 'success',
+					'支付宝': '',
+				},
 				replaceValue: {
 					'微信': '微信',
 					'支付宝': '支付宝',
@@ -87,16 +94,18 @@ const baTable = new baTableClass(
 				align: 'center',
 				render: 'tag',
 				custom: {
-					'待审核': 'warning',
-					'已通过': 'success',
-					'已拒绝': 'danger',
-					'已取消': 'info',
+					'0': 'warning',
+					'1': 'success',
+					'2': 'danger',
+					'3': '',
+					'4': 'danger',
 				},
 				replaceValue: {
-					'待审核': '待审核',
-					'已通过': '已通过',
-					'已拒绝': '已拒绝',
-					'已取消': '已取消',
+					'0': '待审核',
+					'1': '已通过',
+					'2': '已拒绝',
+					'3': '已打款',
+					'4': '打款失败',
 				},
 				minWidth: 100,
 			},

+ 4 - 1
web/src/views/backend/package/package/index.vue

@@ -23,7 +23,7 @@
             <template #operate>
                 <el-table-column prop="operate" align="center" label="操作" min-width="220">
                     <template #default="scope">
-                        <el-button type="info" size="small" @click="onDetail(scope.row)"  v-auth="'detail'">详情</el-button>
+                        <el-button type="primary" plain size="small" @click="onDetail(scope.row)"  v-auth="'detail'">详情</el-button>
                         <el-button type="primary" size="small" @click="onEdit(scope.row)" v-auth="'edit'">编辑</el-button>
                         <el-button type="danger" size="small" @click="handleDel(scope.row)" v-auth="'delete'">删除</el-button>
                     </template>
@@ -122,6 +122,9 @@ const baTable = new baTableClass(
             },
         ],
         dblClickNotEditColumn: [undefined,],
+        filter:{
+            limit:20
+        }
     },
     {
         defaultItems: {

+ 7 - 2
web/src/views/backend/package/package/popupForm.vue

@@ -33,6 +33,11 @@
 							<div v-loading="specLoading" class="spec-list">
 								<el-table :data="specListData" border stripe style="width: 100%">
 									<el-table-column prop="spec_name" align="center" label="规格名称" min-width="120" />
+									<el-table-column prop="image" align="center" label="图片" min-width="100">
+										<template #default="scope">
+											<el-image :src="scope.row.image" style="width: 50px; height: 50px;" />
+										</template>
+									</el-table-column>
 									<el-table-column prop="price" align="center" label="价格" min-width="100">
 										<template #default="scope">
 											<span>¥{{ scope.row.price }}</span>
@@ -41,8 +46,8 @@
 									<el-table-column prop="stock" align="center" label="库存" min-width="80" />
 									<el-table-column prop="type" align="center" label="套餐类型" min-width="100">
 										<template #default="scope">
-											<el-tag :type="scope.row.type === '1' ? 'primary' : 'success'">
-												{{ scope.row.type === '1' ? '个人套餐' : '企业套餐' }}
+											<el-tag :type="scope.row.type == '1' ? 'primary' : 'success'">
+												{{ scope.row.type == '1' ? '个人套餐' : '企业套餐' }}
 											</el-tag>
 										</template>
 									</el-table-column>

+ 11 - 2
web/src/views/backend/package/package/specDialog.vue

@@ -31,6 +31,8 @@
 							show-word-limit
 						/>
 					</el-form-item>
+					<FormItem label="规格图片" prop="image" type="image" v-model="formData.image"
+                        :input-attr="{ returnFullUrl: true }" />
 					
 					<el-row :gutter="20">
 						<el-col :span="12">
@@ -71,7 +73,6 @@
 									v-model="formData.type" 
 									placeholder="请选择套餐类型" 
 									style="width: 100%"
-									clearable
 								>
 									<el-option
 										v-for="opt in packageTypeOptions"
@@ -89,7 +90,6 @@
 									placeholder="请选择卡类型"
 									style="width: 100%"
 									@change="handleCardTypeChange"
-									clearable
 								>
 									<el-option
 										v-for="opt in cardTypeOptions"
@@ -166,6 +166,7 @@
 import { ref, reactive } from 'vue'
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
 import { useI18n } from 'vue-i18n'
+import FormItem from '/@/components/formItem/index.vue'
 import { editSpec } from '/@/api/backend/package/package'
 
 const { t } = useI18n()
@@ -187,6 +188,7 @@ const emits = defineEmits(['success'])
 
 const formData = reactive({
 	spec_name: '',
+	image:'' as string,
 	price: undefined as number | undefined,
 	stock: undefined as number | undefined,
 	type: '',
@@ -201,6 +203,9 @@ const rules: FormRules = {
 	spec_name: [
 		{ required: true, message: '请输入规格名称', trigger: 'change' }
 	],
+	image: [
+		{ required: true, message: '请上传规格图片', trigger: 'change' }
+	],
 	price: [
 		{ required: true, message: '请输入价格', trigger: 'change' }
 	],
@@ -282,6 +287,8 @@ const editDialog = (pkgId: number, specData: any) => {
 
 	// 回填数据,确保数字类型正确转换
 	formData.spec_name = specData.spec_name || specData.specName || ''
+	// 规格图片
+	formData.image = specData.image || ''
 	// 价格:转换为数字,如果为空或无效则设为 undefined
 	formData.price = toNumber(specData.price)
 	// 库存:转换为数字,如果为空或无效则设为 undefined
@@ -301,6 +308,7 @@ const editDialog = (pkgId: number, specData: any) => {
 const resetForm = () => {
 	formRef.value?.clearValidate()
 	formData.spec_name = ''
+	formData.image = ''
 	formData.price = undefined
 	formData.stock = undefined
 	formData.type = ''
@@ -334,6 +342,7 @@ const onSubmit = async () => {
 			id: currentSpecId.value || '',
 			package_id: packageId.value,
 			spec_name: formData.spec_name,
+			image: formData.image,
 			// 确保数字类型正确
 			price: formData.price != null ? Number(formData.price) : undefined,
 			stock: formData.stock != null ? Number(formData.stock) : undefined,

+ 10 - 0
web/src/views/backend/package/product/index.vue

@@ -155,6 +155,12 @@ const baTable = new baTableClass(
                 align: 'center',
                 minWidth: 130
             },
+            {
+                label: '排序',
+                prop: 'sort',
+                align: 'center',
+                minWidth: 80
+            },
             {
                 label: '产品描述',
                 prop: 'description',
@@ -177,6 +183,9 @@ const baTable = new baTableClass(
             },
         ],
         dblClickNotEditColumn: [undefined,],
+        filter:{
+            limit:10
+        }
     },
     {
         defaultItems: {
@@ -188,6 +197,7 @@ const baTable = new baTableClass(
             status: '1',
             type: '1',
             link: '',
+            sort:1,
         },
     },
     {

+ 8 - 3
web/src/views/backend/package/product/popupForm.vue

@@ -22,7 +22,8 @@
                                 :value="item.value" />
                         </el-select>
                     </el-form-item>
-                    <FormItem label="产品图片" prop="image" type="image" v-model="baTable.form.items!.image" :input-attr="{ returnFullUrl: true }" />
+                    <FormItem label="产品图片" prop="image" type="image" v-model="baTable.form.items!.image"
+                        :input-attr="{ returnFullUrl: true }" />
                     <FormItem label="产品状态" prop="status" type="radio" v-model="baTable.form.items!.status"
                         :input-attr="{ border: true, content: statusOptions }" />
                     <FormItem label="产品类型" prop="type" type="radio" v-model="baTable.form.items!.type"
@@ -30,7 +31,9 @@
                     <el-form-item label="产品链接" prop="link">
                         <el-input v-model="baTable.form.items!.link" placeholder="请输入产品链接" />
                     </el-form-item>
-
+                    <el-form-item label="排序" prop="sort">
+                        <el-input-number :min="0" v-model="baTable.form.items!.sort" placeholder="请输入产品排序顺序" />
+                    </el-form-item>
                     <el-form-item label="产品描述" prop="description">
                         <el-input v-model="baTable.form.items!.description" type="textarea" :rows="4"
                             placeholder="请输入产品描述" />
@@ -43,7 +46,7 @@
                 <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
                 <el-button v-blur :loading="baTable.form.submitLoading" @click="btnSubmit" type="primary">
                     {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') :
-                    t('Save') }}
+                        t('Save') }}
                 </el-button>
             </div>
         </template>
@@ -72,6 +75,7 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
     name: [buildValidatorData({ name: 'required', title: '产品名称' })],
     package_id: [buildValidatorData({ name: 'required', trigger: 'change', title: '关联套餐' })],
     description: [buildValidatorData({ name: 'required', title: '产品描述' })],
+    sort: [buildValidatorData({ name: 'required', title: '产品排序' })],
     image: [buildValidatorData({ name: 'required', title: '产品图片' })],
     type: [buildValidatorData({ name: 'required', title: '产品类型' })],
     link: [buildValidatorData({ name: 'required', title: '产品链接' })],
@@ -103,6 +107,7 @@ const btnSubmit = () => {
                 link: baTable.form.items!.link,
                 status: baTable.form.items!.status,
                 description: baTable.form.items!.description,
+                sort: baTable.form.items!.sort,
             }
 
             edit(params).then(() => {

+ 24 - 10
web/src/views/backend/pay/accountConfig/index.vue

@@ -94,14 +94,19 @@ const baTable = new baTableClass(
 			},
 			{
 				label: '账户类型',
-				prop: 'account_type_text',
+				prop: 'account_type',
 				align: 'center',
 				minWidth: 100,
 				render: 'tag',
 				custom: {
-					'银行卡': 'warning',
-					'支付宝': '',
-					'微信': 'success',
+					'bank': 'warning',
+					'alipay': '',
+					'wechat': 'success',
+				},
+				replaceValue: {
+					'bank': '银行卡',
+					'alipay': '支付宝',
+					'wechat': '微信',
 				},
 			},
 			{ 
@@ -124,23 +129,32 @@ const baTable = new baTableClass(
 			},
 			{
 				label: '是否默认',
-				prop: 'is_default_text',
+				prop: 'is_default',
 				align: 'center',
 				render: 'tag',
+				effect:'dark',
 				custom: {
-					'是': '',
-					'否': 'info',
+					'1': '',
+					'0': 'info',
+				},
+				replaceValue: {
+					'1': '是',
+					'0': '否',
 				},
 				minWidth: 100,
 			},
 			{
 				label: '状态',
-				prop: 'status_text',
+				prop: 'status',
 				align: 'center',
 				render: 'tag',
 				custom: {
-					'禁用': 'danger',
-					'启用': 'success',
+					'2': 'danger',
+					'1': 'success',
+				},
+				replaceValue: {
+					'1': '启用',
+					'2': '禁用',
 				},
 				minWidth: 100,
 			},

+ 3 - 0
web/src/views/backend/userCenter/index/index.vue

@@ -98,6 +98,9 @@ const baTable = new baTableClass(
             // },
         ],
         dblClickNotEditColumn: [undefined,],
+        filter:{
+            limit:20
+        }
     },
     {
         defaultItems: {

+ 443 - 275
web/src/views/backend/userCenter/user/detailDialog.vue

@@ -1,12 +1,6 @@
 <template>
-    <el-dialog
-        v-model="visible"
-        class="ba-operate-dialog user-detail-dialog"
-        width="1200px"
-        :close-on-click-modal="false"
-        :destroy-on-close="true"
-        @close="handleClose"
-    >
+    <el-dialog v-model="visible" class="ba-operate-dialog user-detail-dialog" width="1200px"
+        :close-on-click-modal="false" :destroy-on-close="true" @close="handleClose">
         <template #header>
             <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
                 详情
@@ -19,28 +13,87 @@
                 <div class="detail-section">
                     <div class="section-title">用户信息</div>
                     <el-descriptions :column="2" border size="small" class="user-info-descriptions">
-                        <el-descriptions-item label="用户名称">
-                            {{ userInfo.name || '-' }}
+                        <el-descriptions-item label="用户ID">
+                            {{ userInfo.id || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="用户名">
+                            {{ userInfo.username || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="昵称">
+                            {{ userInfo.nickname || '-' }}
                         </el-descriptions-item>
                         <el-descriptions-item label="邮箱">
                             {{ userInfo.email || '-' }}
                         </el-descriptions-item>
-                        <el-descriptions-item label="企业">
-                            {{ userInfo.company || '-' }}
+                        <el-descriptions-item label="积分">
+                            {{ userInfo.score || 0 }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="余额">
+                            {{ userInfo.money || '0.00' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="个性签名">
+                            {{ userInfo.motto || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="企业ID">
+                            {{ userInfo.companyId || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="企业名称">
+                            {{ userInfo.companyName || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="邀请码">
+                            {{ userInfo.invitationCode || '-' }}
                         </el-descriptions-item>
                         <el-descriptions-item label="状态">
-                            <el-tag :type="userInfo.status === '正常' ? 'success' : 'danger'" size="small">
-                                {{ userInfo.status || '-' }}
+                            <el-tag :type="userInfo.status === 'enable' ? 'success' : 'danger'" size="small">
+                                {{ userInfo.statusText || '-' }}
                             </el-tag>
                         </el-descriptions-item>
-                        <el-descriptions-item label="获得总佣金金额">
-                            {{ userInfo.totalCommission || '-' }}
+                        <el-descriptions-item label="注册时间">
+                            {{ userInfo.joinTimeStr || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="注册IP">
+                            {{ userInfo.joinIp || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="最后登录时间">
+                            {{ userInfo.lastLoginTimeStr || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="最后登录IP">
+                            {{ userInfo.lastLoginIp || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="登录失败次数">
+                            {{ userInfo.loginFailure || 0 }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="创建时间">
+                            {{ userInfo.createTimeStr || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="更新时间">
+                            {{ userInfo.updateTimeStr || '-' }}
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </div>
+
+                <!-- 佣金信息部分 -->
+                <div class="detail-section">
+                    <div class="section-title">佣金信息</div>
+                    <el-descriptions :column="2" border size="small" class="commission-info-descriptions">
+                        <el-descriptions-item label="总佣金金额">
+                            {{ commissionInfo.totalCommission || '0.00' }}
                         </el-descriptions-item>
                         <el-descriptions-item label="当前佣金金额">
-                            {{ userInfo.currentCommission || '-' }}
+                            {{ commissionInfo.currentCommission || '0.00' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="冻结中佣金">
+                            {{ commissionInfo.frozenCommission || '0.00' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="已提现佣金">
+                            {{ commissionInfo.withdrawnCommission || '0.00' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="推广链接">
+                            <span class="promotion-link" @click="handleCopyLink(commissionInfo.promotionLink)">{{
+                                commissionInfo.promotionLink || '-' }}</span>
                         </el-descriptions-item>
-                        <el-descriptions-item label="推广链接" :span="2">
-                            {{ userInfo.promotionLink || '-' }}
+                        <el-descriptions-item label="邀请人数">
+                            {{ commissionInfo.invitationCount || 0 }}
                         </el-descriptions-item>
                     </el-descriptions>
                 </div>
@@ -48,99 +101,54 @@
                 <!-- 购买产品部分 -->
                 <div class="detail-section">
                     <div class="section-title">购买产品</div>
-                    <el-table
-                        :data="purchasedProducts"
-                        border
-                        size="small"
-                        style="width: 100%"
-                        v-loading="productsLoading"
-                    >
-                        <el-table-column
-                            prop="productName"
-                            label="产品名称"
-                            align="center"
-                            min-width="120"
-                        />
-                        <el-table-column
-                            prop="packageType"
-                            label="套餐类型"
-                            align="center"
-                            min-width="140"
-                        />
-                        <el-table-column
-                            prop="remainingPoints"
-                            label="剩余积分"
-                            align="center"
-                            min-width="100"
-                        >
+                    <el-table :data="purchasedProducts" border size="small" style="width: 100%"
+                        v-loading="productsLoading">
+                        <el-table-column prop="productName" label="产品名称" align="center" min-width="150" />
+                        <el-table-column prop="displayName" label="套餐名称" align="center" min-width="180" />
+                        <el-table-column prop="cardTypeText" label="卡类型" align="center" min-width="100" />
+                        <el-table-column prop="packageTypeText" label="套餐类型" align="center" min-width="100" />
+                        <el-table-column prop="remainingPoints" label="剩余积分" align="center" min-width="100">
                             <template #default="{ row }">
-                                {{ row.remainingPoints || '--' }}
+                                {{ row.remainingPoints !== undefined && row.remainingPoints !== null ?
+                                row.remainingPoints : '--' }}
                             </template>
                         </el-table-column>
-                        <el-table-column
-                            prop="remainingTimes"
-                            label="剩余次数"
-                            align="center"
-                            min-width="100"
-                        >
+                        <el-table-column prop="remainingTimes" label="剩余次数" align="center" min-width="100">
                             <template #default="{ row }">
-                                {{ row.remainingTimes || '--' }}
+                                {{ row.remainingTimes !== undefined && row.remainingTimes !== null ? row.remainingTimes
+                                : '--' }}
                             </template>
                         </el-table-column>
-                        <el-table-column
-                            prop="expirationDate"
-                            label="到期时间"
-                            align="center"
-                            min-width="120"
-                        >
+                        <el-table-column prop="startTime" label="开始时间" align="center" min-width="160" />
+                        <el-table-column prop="endTime" label="结束时间" align="center" min-width="160" />
+                        <el-table-column prop="expireTime" label="到期时间" align="center" min-width="120" />
+                        <el-table-column prop="buyTime" label="购买时间" align="center" min-width="160" />
+                        <el-table-column prop="lastUseTime" label="最后使用时间" align="center" min-width="160">
                             <template #default="{ row }">
-                                {{ row.expirationDate || '--' }}
+                                {{ row.lastUseTime || '--' }}
                             </template>
                         </el-table-column>
-                        <el-table-column
-                            prop="purchaseDate"
-                            label="购买时间"
-                            align="center"
-                            min-width="120"
-                        />
-                        <el-table-column
-                            prop="status"
-                            label="状态"
-                            align="center"
-                            min-width="100"
-                        >
+                        <el-table-column prop="statusText" label="状态" align="center" min-width="100">
                             <template #default="{ row }">
-                                <el-tag
-                                    :type="row.status === '正常' ? 'success' : 'danger'"
-                                    size="small"
-                                >
-                                    {{ row.status }}
+                                <el-tag :type="row.status === 1 ? 'success' : row.status === 2 ? 'warning' : 'danger'"
+                                    size="small">
+                                    {{ row.statusText }}
                                 </el-tag>
                             </template>
                         </el-table-column>
                     </el-table>
                     <!-- 购买产品分页 -->
                     <div class="pagination-wrapper">
-                        <el-pagination
-                            v-model:current-page="productsPage"
-                            :page-size="productsPageSize"
-                            :total="productsTotal"
-                            layout="prev, pager, next, ->, jumper"
-                            small
-                            @current-change="handleProductsPageChange"
-                        >
+                        <el-pagination v-model:current-page="productsPage" :page-size="productsPageSize"
+                            :total="productsTotal" layout="prev, pager, next, ->, jumper" small
+                            @current-change="handleProductsPageChange">
                             <template #jumper>
                                 <span class="pagination-jumper">
                                     前往
-                                    <el-input-number
-                                        v-model="productsPage"
-                                        :min="1"
-                                        :max="Math.ceil(productsTotal / productsPageSize)"
-                                        size="small"
-                                        controls-position="right"
-                                        style="width: 60px; margin: 0 4px"
-                                        @change="(val: number | undefined) => val && handleProductsPageChange(val)"
-                                    />
+                                    <el-input-number v-model="productsPage" :min="1"
+                                        :max="Math.ceil(productsTotal / productsPageSize)" size="small"
+                                        controls-position="right" style="width: 60px; margin: 0 4px"
+                                        @change="(val: number | undefined) => val && handleProductsPageChange(val)" />
                                     页
                                 </span>
                             </template>
@@ -151,68 +159,30 @@
                 <!-- 下级人员部分 -->
                 <div class="detail-section">
                     <div class="section-title">下级人员</div>
-                    <div class="search-wrapper">
-                        <el-input
-                            v-model="subordinateSearchName"
-                            placeholder="人员名称"
-                            clearable
-                            size="small"
-                            style="width: 200px"
-                            @input="handleSubordinateSearch"
-                        />
-                    </div>
-                    <el-table
-                        :data="subordinatePersonnel"
-                        border
-                        size="small"
-                        style="width: 100%; margin-top: 12px"
-                        v-loading="subordinatesLoading"
-                    >
-                        <el-table-column
-                            prop="personnelName"
-                            label="人员名称"
-                            align="center"
-                            min-width="120"
-                        />
-                        <el-table-column
-                            prop="commissionAmount"
-                            label="产生分佣金额"
-                            align="center"
-                            min-width="140"
-                        />
-                        <el-table-column
-                            prop="status"
-                            label="状态"
-                            align="center"
-                            min-width="100"
-                        >
+                    <el-table :data="subordinatePersonnel" border size="small" style="width: 100%; margin-top: 12px"
+                        v-loading="subordinatesLoading">
+                        <el-table-column prop="name" label="人员名称" align="center" min-width="120" />
+                        <el-table-column prop="username" label="用户名" align="center" min-width="120" />
+                        <el-table-column prop="email" label="邮箱" align="center" min-width="180" />
+                        <el-table-column prop="commissionAmount" label="产生分佣金额" align="center" min-width="140" />
+                        <el-table-column prop="currentCommission" label="当前佣金金额" align="center" min-width="140">
+                            <template #default="{ row }">
+                                {{ row.currentCommission !== null && row.currentCommission !== undefined ?
+                                    row.currentCommission : '0.00' }}
+                            </template>
+                        </el-table-column>
+                        <el-table-column prop="statusText" label="状态" align="center" min-width="100">
                             <template #default="{ row }">
-                                <el-tag
-                                    :type="row.status === '正常' ? 'success' : 'danger'"
-                                    size="small"
-                                >
-                                    {{ row.status }}
+                                <el-tag :type="row.statusText === '正常' ? 'success' : 'danger'" size="small">
+                                    {{ row.statusText }}
                                 </el-tag>
                             </template>
                         </el-table-column>
-                        <el-table-column
-                            prop="bindingTime"
-                            label="绑定时间"
-                            align="center"
-                            min-width="120"
-                        />
-                        <el-table-column
-                            label="操作"
-                            align="center"
-                            min-width="100"
-                        >
+                        <el-table-column prop="bindTime" label="绑定时间" align="center" min-width="160" />
+                        <el-table-column prop="createTime" label="创建时间" align="center" min-width="160" />
+                        <el-table-column label="操作" align="center" min-width="100" fixed="right">
                             <template #default="{ row }">
-                                <el-button
-                                    type="danger"
-                                    link
-                                    size="small"
-                                    @click="handleUnbind(row)"
-                                >
+                                <el-button type="danger" link size="small" @click="handleUnbind(row)">
                                     解除
                                 </el-button>
                             </template>
@@ -220,26 +190,16 @@
                     </el-table>
                     <!-- 下级人员分页 -->
                     <div class="pagination-wrapper">
-                        <el-pagination
-                            v-model:current-page="subordinatesPage"
-                            :page-size="subordinatesPageSize"
-                            :total="subordinatesTotal"
-                            layout="prev, pager, next, ->, jumper"
-                            small
-                            @current-change="handleSubordinatesPageChange"
-                        >
+                        <el-pagination v-model:current-page="subordinatesPage" :page-size="subordinatesPageSize"
+                            :total="subordinatesTotal" layout="prev, pager, next, ->, jumper" small
+                            @current-change="handleSubordinatesPageChange">
                             <template #jumper>
                                 <span class="pagination-jumper">
                                     前往
-                                    <el-input-number
-                                        v-model="subordinatesPage"
-                                        :min="1"
-                                        :max="Math.ceil(subordinatesTotal / subordinatesPageSize)"
-                                        size="small"
-                                        controls-position="right"
-                                        style="width: 60px; margin: 0 4px"
-                                        @change="(val: number | undefined) => val && handleSubordinatesPageChange(val)"
-                                    />
+                                    <el-input-number v-model="subordinatesPage" :min="1"
+                                        :max="Math.ceil(subordinatesTotal / subordinatesPageSize)" size="small"
+                                        controls-position="right" style="width: 60px; margin: 0 4px"
+                                        @change="(val: number | undefined) => val && handleSubordinatesPageChange(val)" />
                                     页
                                 </span>
                             </template>
@@ -260,168 +220,353 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, watch } from 'vue'
+import { ref, reactive, computed } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { detail, removeSubordinate } from '/@/api/backend/userCenter/user'
 
 interface UserInfo {
-    name: string
+    id: number
+    username: string
+    nickname: string
     email: string
-    company: string
+    mobile: string
+    avatar: string
+    gender: number
+    genderText: string
+    birthday: string | null
+    money: number | string
+    score: number
+    lastLoginTime: number
+    lastLoginTimeStr: string
+    lastLoginIp: string
+    loginFailure: number
+    joinIp: string
+    joinTime: number
+    joinTimeStr: string
+    motto: string
     status: string
-    totalCommission: number | string
-    currentCommission: number | string
+    statusText: string
+    updateTime: number
+    updateTimeStr: string
+    createTime: number
+    createTimeStr: string
+    invitationCode: string
+    companyId: number
+    companyName: string
+}
+
+interface CommissionInfo {
+    totalCommission: string
+    currentCommission: string
+    frozenCommission: string
+    withdrawnCommission: string
     promotionLink: string
+    invitationCount: number
 }
 
 interface PurchasedProduct {
+    id: number
     productName: string
-    packageType: string
-    remainingPoints?: string
-    remainingTimes?: string | number
-    expirationDate?: string
-    purchaseDate: string
-    status: string
+    packageName: string
+    packageFullName: string
+    displayName: string
+    cardTypeText: string
+    packageType: number
+    packageTypeText: string
+    remainingPoints: number
+    remainingTimes: number
+    startTime: string
+    endTime: string
+    expireTime: string
+    buyTime: string
+    lastUseTime: string | null
+    status: number
+    statusText: string
 }
 
 interface SubordinatePersonnel {
-    personnelName: string
-    commissionAmount: number
-    status: string
-    bindingTime: string
+    id: number
+    username: string
+    nickname: string
+    name: string
+    email: string
+    currentCommission: string | null
+    commissionAmount: string
+    statusText: string
+    bindTime: string
+    createTime: string
 }
 
 const visible = ref(false)
+const loading = ref(false)
+
 const userInfo = reactive<UserInfo>({
-    name: '',
+    id: 0,
+    username: '',
+    nickname: '',
     email: '',
-    company: '',
+    mobile: '',
+    avatar: '',
+    gender: 0,
+    genderText: '',
+    birthday: null,
+    money: 0,
+    score: 0,
+    lastLoginTime: 0,
+    lastLoginTimeStr: '',
+    lastLoginIp: '',
+    loginFailure: 0,
+    joinIp: '',
+    joinTime: 0,
+    joinTimeStr: '',
+    motto: '',
     status: '',
-    totalCommission: '',
-    currentCommission: '',
+    statusText: '',
+    updateTime: 0,
+    updateTimeStr: '',
+    createTime: 0,
+    createTimeStr: '',
+    invitationCode: '',
+    companyId: 0,
+    companyName: '',
+})
+
+const commissionInfo = reactive<CommissionInfo>({
+    totalCommission: '0.00',
+    currentCommission: '0.00',
+    frozenCommission: '0.00',
+    withdrawnCommission: '0.00',
     promotionLink: '',
+    invitationCount: 0,
 })
 
 // 购买产品相关
-const purchasedProducts = ref<PurchasedProduct[]>([])
+const allProducts = ref<PurchasedProduct[]>([])
 const productsLoading = ref(false)
 const productsPage = ref(1)
 const productsPageSize = ref(10)
-const productsTotal = ref(0)
 
 // 下级人员相关
-const subordinatePersonnel = ref<SubordinatePersonnel[]>([])
+const allSubordinates = ref<SubordinatePersonnel[]>([])
 const subordinatesLoading = ref(false)
 const subordinateSearchName = ref('')
 const subordinatesPage = ref(1)
 const subordinatesPageSize = ref(10)
-const subordinatesTotal = ref(0)
 
-// 打开弹框
-const open = (row: any) => {
-    // 设置用户信息
-    userInfo.name = row.name || '张三'
-    userInfo.email = row.email || '9854112544@qq.com'
-    userInfo.company = row.company || '腾达'
-    userInfo.status = '正常'
-    userInfo.totalCommission = 500
-    userInfo.currentCommission = 100
-    userInfo.promotionLink = 'XXXX.com?from=用户ID'
-
-    // 加载购买产品数据
-    loadPurchasedProducts()
-
-    // 加载下级人员数据
-    loadSubordinatePersonnel()
+// 计算属性:分页后的产品列表
+const purchasedProducts = computed(() => {
+    const start = (productsPage.value - 1) * productsPageSize.value
+    const end = start + productsPageSize.value
+    return allProducts.value.slice(start, end)
+})
 
-    visible.value = true
-}
+// 计算属性:产品总数
+const productsTotal = computed(() => allProducts.value.length)
 
-// 加载购买产品数据
-const loadPurchasedProducts = () => {
-    productsLoading.value = true
-    // 模拟异步请求
-    setTimeout(() => {
-        const mockData: PurchasedProduct[] = [
-            {
-                productName: '聚德神笔',
-                packageType: '日卡',
-                remainingPoints: '--',
-                remainingTimes: '--',
-                expirationDate: '2025-10-11',
-                purchaseDate: '2025-10-11',
-                status: '已过期',
-            },
-            {
-                productName: '棱镜',
-                packageType: '棱镜10次卡',
-                remainingPoints: '--',
-                remainingTimes: 10,
-                expirationDate: '--',
-                purchaseDate: '2025-10-11',
-                status: '正常',
-            },
-        ]
-        purchasedProducts.value = mockData
-        productsTotal.value = mockData.length
-        productsLoading.value = false
-    }, 300)
+// 计算属性:过滤后的下级人员列表
+const filteredSubordinates = computed(() => {
+    if (!subordinateSearchName.value) {
+        return allSubordinates.value
+    }
+    const search = subordinateSearchName.value.toLowerCase()
+    return allSubordinates.value.filter(
+        (item) =>
+            item.name?.toLowerCase().includes(search) ||
+            item.username?.toLowerCase().includes(search) ||
+            item.email?.toLowerCase().includes(search)
+    )
+})
+
+// 计算属性:分页后的下级人员列表
+const subordinatePersonnel = computed(() => {
+    const start = (subordinatesPage.value - 1) * subordinatesPageSize.value
+    const end = start + subordinatesPageSize.value
+    return filteredSubordinates.value.slice(start, end)
+})
+
+// 计算属性:下级人员总数
+const subordinatesTotal = computed(() => filteredSubordinates.value.length)
+
+
+// 获取状态文本
+const getStatusText = (status: string): string => {
+    return status === 'enable' ? '启用' : '禁用'
 }
 
-// 加载下级人员数据
-const loadSubordinatePersonnel = () => {
-    subordinatesLoading.value = true
-    // 模拟异步请求
-    setTimeout(() => {
-        let mockData: SubordinatePersonnel[] = [
-            {
-                personnelName: '王五',
-                commissionAmount: 0,
-                status: '正常',
-                bindingTime: '2025-10-11',
-            },
-            {
-                personnelName: '李四',
-                commissionAmount: 10,
-                status: '正常',
-                bindingTime: '2025-10-11',
-            },
-        ]
-
-        // 根据搜索条件过滤
-        if (subordinateSearchName.value) {
-            mockData = mockData.filter(item =>
-                item.personnelName.includes(subordinateSearchName.value)
-            )
-        }
+// 获取套餐类型文本
+const getPackageTypeText = (type: number): string => {
+    return type === 1 ? '个人' : '企业'
+}
 
-        subordinatePersonnel.value = mockData
-        subordinatesTotal.value = mockData.length
-        subordinatesLoading.value = false
-    }, 300)
+// 打开弹框
+const open = (row: TableRow) => {
+    visible.value = true
+    loading.value = true
+
+    detail({ id: row.id })
+        .then((res: anyObj) => {
+            const data = res.data?.data || res.data
+
+            if (data) {
+                // 处理用户信息
+                if (data.user_info) {
+                    const user = data.user_info
+                    Object.assign(userInfo, {
+                        id: user.id || 0,
+                        username: user.username || '',
+                        nickname: user.nickname || '',
+                        email: user.email || '',
+                        mobile: user.mobile || '',
+                        avatar: user.avatar || '',
+                        gender: user.gender || 0,
+                        birthday: user.birthday || null,
+                        money: user.money || 0,
+                        score: user.score || 0,
+                        lastLoginTime: user.last_login_time || 0,
+                        lastLoginTimeStr: user.last_login_time_str || '',
+                        lastLoginIp: user.last_login_ip || '',
+                        loginFailure: user.login_failure || 0,
+                        joinIp: user.join_ip || '',
+                        joinTime: user.join_time || 0,
+                        joinTimeStr: user.join_time_str || '',
+                        motto: user.motto || '',
+                        status: user.status || '',
+                        statusText: getStatusText(user.status || ''),
+                        updateTime: user.update_time || 0,
+                        updateTimeStr: user.update_time_str || '',
+                        createTime: user.create_time || 0,
+                        createTimeStr: user.create_time_str || '',
+                        invitationCode: user.invitation_code || '',
+                        companyId: user.company_id || 0,
+                        companyName: user.company_name || '',
+                    })
+                }
+
+                // 处理佣金信息
+                if (data.commission_info) {
+                    const commission = data.commission_info
+                    Object.assign(commissionInfo, {
+                        totalCommission: commission.total_commission || '0.00',
+                        currentCommission: commission.current_commission || '0.00',
+                        frozenCommission: commission.frozen_commission || '0.00',
+                        withdrawnCommission: commission.withdrawn_commission || '0.00',
+                        promotionLink: commission.promotion_link || '',
+                        invitationCount: commission.invitation_count || 0,
+                    })
+                }
+
+                // 处理产品列表
+                if (data.product_list && Array.isArray(data.product_list)) {
+                    allProducts.value = data.product_list.map((item: any) => ({
+                        id: item.id || 0,
+                        productName: item.product_name || '',
+                        packageName: item.package_name || '',
+                        packageFullName: item.package_full_name || '',
+                        displayName: item.display_name || '',
+                        cardTypeText: item.card_type_text || '',
+                        packageType: item.package_type || 0,
+                        packageTypeText: getPackageTypeText(item.package_type || 0),
+                        remainingPoints: item.remaining_points || 0,
+                        remainingTimes: item.remaining_times || 0,
+                        startTime: item.start_time || '',
+                        endTime: item.end_time || '',
+                        expireTime: item.expire_time || '',
+                        buyTime: item.buy_time || '',
+                        lastUseTime: item.last_use_time || null,
+                        status: item.status || 0,
+                        statusText: item.status_text || '',
+                    }))
+                } else {
+                    allProducts.value = []
+                }
+
+                // 处理下级人员列表
+                if (data.subordinate_list && Array.isArray(data.subordinate_list)) {
+                    allSubordinates.value = data.subordinate_list.map((item: any) => ({
+                        id: item.id || 0,
+                        username: item.username || '',
+                        nickname: item.nickname || '',
+                        name: item.name || '',
+                        email: item.email || '',
+                        currentCommission: item.current_commission,
+                        commissionAmount: item.commission_amount || '0.00',
+                        statusText: item.status_text || '',
+                        bindTime: item.bind_time || '',
+                        createTime: item.create_time || '',
+                    }))
+                } else {
+                    allSubordinates.value = []
+                }
+
+                // 重置分页
+                productsPage.value = 1
+                subordinatesPage.value = 1
+            }
+        })
+        .catch((error: any) => {
+            ElMessage.error(error?.msg || '获取详情失败')
+        })
+        .finally(() => {
+            loading.value = false
+        })
 }
 
 // 购买产品分页变化
 const handleProductsPageChange = (page: number) => {
     productsPage.value = page
-    loadPurchasedProducts()
 }
 
 // 下级人员分页变化
 const handleSubordinatesPageChange = (page: number) => {
     subordinatesPage.value = page
-    loadSubordinatePersonnel()
 }
 
 // 下级人员搜索
 const handleSubordinateSearch = () => {
     subordinatesPage.value = 1
-    loadSubordinatePersonnel()
+}
+
+// 复制推广链接
+const handleCopyLink = (link: string) => {
+    if (!link) {
+        ElMessage.warning('推广链接为空')
+        return
+    }
+
+    // 使用 Clipboard API 复制
+    if (navigator.clipboard) {
+        navigator.clipboard.writeText(link).then(() => {
+            ElMessage.success('复制成功')
+        }).catch(() => {
+            // 降级方案
+            fallbackCopy(link)
+        })
+    } else {
+        fallbackCopy(link)
+    }
+}
+
+// 降级复制方案
+const fallbackCopy = (text: string) => {
+    const textarea = document.createElement('textarea')
+    textarea.value = text
+    textarea.style.position = 'fixed'
+    textarea.style.opacity = '0'
+    document.body.appendChild(textarea)
+    textarea.select()
+    try {
+        document.execCommand('copy')
+        ElMessage.success('复制成功')
+    } catch (err) {
+        ElMessage.error('复制失败')
+    }
+    document.body.removeChild(textarea)
 }
 
 // 解除绑定
 const handleUnbind = (row: SubordinatePersonnel) => {
     ElMessageBox.confirm(
-        `确定要解除与"${row.personnelName}"的绑定关系吗?`,
+        `确定要解除与"${row.name}"的绑定关系吗?`,
         '提示',
         {
             confirmButtonText: '确定',
@@ -430,8 +575,22 @@ const handleUnbind = (row: SubordinatePersonnel) => {
         }
     )
         .then(() => {
-            ElMessage.success('解除绑定成功')
-            loadSubordinatePersonnel()
+            removeSubordinate({ id: row.id })
+                .then(() => {
+                    ElMessage.success('解除绑定成功')
+                    // 从列表中移除
+                    const index = allSubordinates.value.findIndex(item => item.id === row.id)
+                    if (index > -1) {
+                        allSubordinates.value.splice(index, 1)
+                    }
+                    // 如果当前页没有数据了,返回上一页
+                    if (subordinatePersonnel.value.length === 0 && subordinatesPage.value > 1) {
+                        subordinatesPage.value--
+                    }
+                })
+                .catch((error: any) => {
+                    ElMessage.error(error?.msg || '解除绑定失败')
+                })
         })
         .catch(() => {
             // 取消操作
@@ -445,6 +604,8 @@ const handleClose = () => {
     subordinateSearchName.value = ''
     productsPage.value = 1
     subordinatesPage.value = 1
+    allProducts.value = []
+    allSubordinates.value = []
 }
 
 defineExpose({ open })
@@ -471,7 +632,8 @@ defineExpose({ open })
         color: #333;
     }
 
-    .user-info-descriptions {
+    .user-info-descriptions,
+    .commission-info-descriptions {
         :deep(.el-descriptions__label) {
             font-weight: 500;
         }
@@ -501,5 +663,11 @@ defineExpose({ open })
         align-items: center;
     }
 }
-</style>
 
+.promotion-link {
+    color: #409EFF;
+    text-decoration: none;
+    font-size: 14px;
+    cursor: pointer;
+}
+</style>

+ 11 - 8
web/src/views/backend/userCenter/user/index.vue

@@ -65,19 +65,19 @@ const baTable = new baTableClass(
             { label: '编号', prop: 'id', align: 'center', width: 70 },
             {
                 label: '用户名',
-                prop: 'name',
+                prop: 'username',
                 align: 'center',
-                minWidth: 120
+                minWidth: 150
             },
             {
                 label: '邮箱',
                 prop: 'email',
                 align: 'center',
-                minWidth: 170
+                minWidth: 200
             },
             {
                 label: '企业',
-                prop: 'company',
+                prop: 'company_names',
                 align: 'center',
                 minWidth: 160
             },
@@ -86,13 +86,13 @@ const baTable = new baTableClass(
                 prop: 'status',
                 align: 'center',
                 render: 'tag',
-                custom: { '1': 'success', '0': 'danger' },
-                replaceValue: { '1': '启用', '0': '禁用' },
-                minWidth: 150
+                custom: { 'enable': 'success', 'disable': 'danger' },
+                replaceValue: { 'enable': '启用', 'disable': '禁用' },
+                minWidth: 120
             },
             {
                 label: '邀请人',
-                prop: 'inviter',
+                prop: 'inviter_names',
                 align: 'center',
                 minWidth: 120
             },
@@ -117,6 +117,9 @@ const baTable = new baTableClass(
             },
         ],
         dblClickNotEditColumn: [undefined,],
+        filter:{
+            limit:10
+        }
     },
     {
         defaultItems: {

+ 7 - 7
web/src/views/backend/userCenter/user/popupForm.vue

@@ -29,9 +29,9 @@
                     :rules="rules"
                     v-if="!baTable.form.loading"
                 >
-                    <el-form-item label="姓名" prop="name">
+                    <el-form-item label="姓名" prop="username">
                         <el-input
-                            v-model="baTable.form.items!.name"
+                            v-model="baTable.form.items!.username"
                             placeholder="请输入姓名"
                         />
                     </el-form-item>
@@ -106,7 +106,7 @@ const emit = defineEmits(['success'])
 const { t } = useI18n()
 
 const rules: Partial<Record<string, FormItemRule[]>> = reactive({
-    name: [buildValidatorData({ name: 'required', title: '姓名' })],
+    username: [buildValidatorData({ name: 'required', title: '姓名' })],
     email: [
         buildValidatorData({ name: 'required', title: '邮箱' }),
         buildValidatorData({ name: 'email', title: '邮箱' })
@@ -135,8 +135,8 @@ interface CompanyOption {
 const companyOptions = ref<CompanyOption[]>([])
 
 const statusOptions = {
-    '0': '禁用',
-    '1': '启用',
+    'disable': '禁用',
+    'enable': '启用',
 }
 
 const getCompanyList = () => {
@@ -156,9 +156,9 @@ const btnSubmit = () => {
 
             let params = {
                 id: baTable.form.items!.id,
-                name: baTable.form.items!.name,
+                name: baTable.form.items!.username,
                 email:baTable.form.items!.email,
-                company:baTable.form.items!.company,
+                company_id:baTable.form.items!.company,
                 password:baTable.form.items!.password,
                 status: baTable.form.items!.status,
             }

+ 26 - 7
web/src/views/frontend/Me.vue

@@ -38,12 +38,14 @@
 </template>
 
 <script setup lang="ts">
-import { computed, markRaw, reactive, ref, type Component } from 'vue'
-import { Goods, HomeFilled, OfficeBuilding, ShoppingCartFull, User } from '@element-plus/icons-vue'
+import { computed, markRaw, onMounted, reactive, ref, watch, type Component } from 'vue'
+import { useRoute } from 'vue-router'
+import { Goods, HomeFilled,Tickets, OfficeBuilding, ShoppingCartFull, User } from '@element-plus/icons-vue'
 import Header from './components/header.vue'
 // 导入各个功能组件
 import Info from './components/Me/info.vue'
 import PurchaseHistory from './components/Me/purchaseHistory.vue'
+import withdrawList from './components/Me/withdrawList.vue'
 import PurchaseProduct from './components/Me/purchaseProduct.vue'
 import Subordinates from './components/Me/subordinates.vue'
 import Enterprise from './components/Me/enterprise.vue'
@@ -52,27 +54,44 @@ defineOptions({ name: 'ClientMe' })
 
 type MenuItem = { label: string; value: string; component: Component; icon: Component }
 
+const route = useRoute()
 const activeMenu = ref('home')
 const menuList = reactive<MenuItem[]>([
   { label: '我的主页', value: 'home', component: markRaw(Info), icon: markRaw(HomeFilled) },
   { label: '购买记录', value: 'purchase-history', component: markRaw(PurchaseHistory), icon: markRaw(ShoppingCartFull) },
+  { label: '提现记录', value: 'withdraw-list', component: markRaw(withdrawList), icon: markRaw(Tickets) },
   { label: '我的产品', value: 'purchase-product', component: markRaw(PurchaseProduct), icon: markRaw(Goods) },
   { label: '我的下级', value: 'subordinates', component: markRaw(Subordinates), icon: markRaw(User) },
   { label: '我的企业', value: 'enterprise', component: markRaw(Enterprise), icon: markRaw(OfficeBuilding) }
 ])
 
-const currentMenuLabel = computed(() => {
-  const current = menuList.find(item => item.value === activeMenu.value)
-  return current ? current.label : ''
-})
-
 const currentComponent = computed(() => {
   const current = menuList.find(item => item.value === activeMenu.value)
   return current ? current.component : Info
 })
 
+// 根据路由query参数设置激活的菜单
+const initActiveMenu = () => {
+  const tab = route.query.tab as string
+  if (tab && menuList.some(item => item.value === tab)) {
+    activeMenu.value = tab
+  }
+}
+
+// 监听路由query变化,更新激活的菜单
+watch(() => route.query.tab, (newTab) => {
+  if (newTab && typeof newTab === 'string' && menuList.some(item => item.value === newTab)) {
+    activeMenu.value = newTab
+  }
+})
+
 const handleMenuChange = (value: any) => {
 }
+
+// 初始化时根据路由query设置激活菜单
+onMounted(() => {
+  initActiveMenu()
+})
 </script>
 
 <style scoped lang="scss">

+ 365 - 116
web/src/views/frontend/Order.vue

@@ -9,7 +9,8 @@
           <!-- 左侧:大图区域 -->
           <div class="goods-cover">
             <div class="cover-image">
-              <img :src="goodsInfo.cover" alt="商品封面">
+              <img v-if="goodsInfo.image" :src="goodsInfo.image" alt="商品封面">
+              <div v-else class="no-image">暂无图片</div>
             </div>
           </div>
 
@@ -21,9 +22,9 @@
                 {{ goodsInfo.name }}
               </h2>
               <div class="goods-tags">
-                <el-tag type="success" size="small">{{ goodsInfo.deliveryType }}</el-tag>
-                <el-tag size="small">已售 {{ goodsInfo.sold }}</el-tag>
-                <el-tag type="warning" size="small">库存 {{ goodsInfo.stockText }}</el-tag>
+                <el-tag type="success" size="small">{{ goodsInfo.type_text }}</el-tag>
+                <el-tag size="small">已售 {{ goodsInfo.sold_count }}</el-tag>
+                <el-tag type="warning" size="small">库存 {{ goodsInfo.stock_text }}</el-tag>
                 <el-tag type="info" size="small">{{ cardTypeText }}</el-tag>
               </div>
             </div>
@@ -39,32 +40,22 @@
               </div>
               <div class="price-desc">
                 <span v-if="isSeatBasedEnterprise">
-                  {{ goodsInfo.comboPeopleNum }} 人席位,总价将按 {{ goodsInfo.comboPrice }} 元/席位计算
+                  企业套餐,按席位计算(单价:{{ goodsInfo.price_text }})
                 </span>
                 <span v-else>
-                  已选择 {{ buyCount }} 件,自动根据数量合计
+                  已选择 {{ buyCount }} 件,自动根据数量合计(单价:{{ goodsInfo.price_text }})
                 </span>
               </div>
             </div>
 
             <!-- 购买数量 -->
-            <div
-              v-if="!isSeatBasedEnterprise"
-              class="field-row quantity-row"
-            >
+            <div v-if="!isSeatBasedEnterprise" class="field-row quantity-row">
               <div class="field-label">
                 <span>购买数量</span>
               </div>
               <div class="field-content quantity-control">
-                <el-input-number
-                  v-model="buyCount"
-                  :min="1"
-                  :max="goodsInfo.stock"
-                  :step="1"
-                  size="small"
-                  controls-position="right"
-                  class="qty-input-number"
-                />
+                <el-input-number v-model="buyCount" :min="1" :max="goodsInfo.stock" :step="1" size="small"
+                  controls-position="right" class="qty-input-number" />
                 <span class="stock-hint">
                   库存剩余:{{ goodsInfo.stock }} 件
                 </span>
@@ -72,24 +63,15 @@
             </div>
 
             <!-- 企业套餐:席位人数 -->
-            <div
-              v-else
-              class="field-row quantity-row"
-            >
+            <div v-else class="field-row quantity-row">
               <div class="field-label">
                 <span>购买席位</span>
               </div>
               <div class="field-content quantity-control">
-                <el-input-number
-                  v-model="goodsInfo.comboPeopleNum"
-                  :min="2"
-                  :step="1"
-                  size="small"
-                  controls-position="right"
-                  class="qty-input-number"
-                />
+                <el-input-number v-model="seatCount" :min="2" :step="1" size="small" controls-position="right"
+                  class="qty-input-number" />
                 <span class="stock-hint">
-                  最少2个席位,当前默认 {{ goodsInfo.comboPeopleNum }} 个
+                  最少2个席位,当前默认 {{ seatCount }} 个
                 </span>
               </div>
             </div>
@@ -100,27 +82,17 @@
                 <span>付款</span>
               </div>
               <div class="field-content pay-methods">
-                <el-button
-                  size="small"
-                  :type="selectedPayMethod === 'alipay' ? 'primary' : 'default'"
-                  :plain="selectedPayMethod !== 'alipay'"
-                  class="pay-btn"
-                  :class="{ active: selectedPayMethod === 'alipay' }"
-                  @click="selectedPayMethod = 'alipay'"
-                >
-<el-icon class="pay-icon alipay">
-  <Coin />
+                <!-- <el-button size="small" :type="selectedPayMethod === 'alipay' ? 'primary' : 'default'"
+                  :plain="selectedPayMethod !== 'alipay'" class="pay-btn"
+                  :class="{ active: selectedPayMethod === 'alipay' }" @click="selectedPayMethod = 'alipay'">
+                  <el-icon class="pay-icon alipay">
+                    <Coin />
                   </el-icon>
                   支付宝
-                </el-button>
-                <el-button
-                  size="small"
-                  :type="selectedPayMethod === 'wechat' ? 'success' : 'default'"
-                  :plain="selectedPayMethod !== 'wechat'"
-                  class="pay-btn"
-                  :class="{ active: selectedPayMethod === 'wechat' }"
-                  @click="selectedPayMethod = 'wechat'"
-                >
+                </el-button> -->
+                <el-button size="small" :type="selectedPayMethod === 'wechat' ? 'success' : 'default'"
+                  :plain="selectedPayMethod !== 'wechat'" class="pay-btn"
+                  :class="{ active: selectedPayMethod === 'wechat' }" @click="selectedPayMethod = 'wechat'">
                   <el-icon class="pay-icon wechat">
                     <ChatDotRound />
                   </el-icon>
@@ -130,12 +102,7 @@
             </div>
 
             <div class="submit-row">
-              <el-button
-                type="primary"
-                size="large"
-                class="submit-btn"
-                @click="handleSubmit"
-              >
+              <el-button type="primary" size="large" class="submit-btn" @click="handleSubmit">
                 立即下单
               </el-button>
             </div>
@@ -144,10 +111,12 @@
       </el-card>
 
       <!-- 创建企业弹框组件 -->
-      <CreateEnterpriseDialog
-        v-model:visible="createEnterpriseVisible"
-        @success="handleEnterpriseCreated"
-      />
+      <CreateEnterpriseDialog v-model:visible="createEnterpriseVisible" @success="handleEnterpriseCreated" />
+
+      <!-- 支付二维码弹窗 -->
+      <PayQrCodeDialog v-model:visible="payDialogVisible" :order-info="currentOrderInfo" :qr-code-url="qrCodeUrl"
+        :pay-method="selectedPayMethod" :loading="qrCodeLoading" :pay-status="payStatus" @close="handlePayDialogClose"
+        @confirm="handlePaySuccess" @expired="handlePayExpired" />
 
       <!-- 下半部分:宝贝详情 -->
       <el-card class="detail-panel" shadow="never">
@@ -158,9 +127,7 @@
         </template>
 
         <div class="detail-content">
-          <p v-for="(line, index) in goodsInfo.detailLines" :key="index">
-            {{ line }}
-          </p>
+          <p>{{ goodsInfo.description || '暂无详情' }}</p>
         </div>
       </el-card>
     </div>
@@ -168,113 +135,386 @@
 </template>
 
 <script setup>
-import { computed, onMounted, reactive, ref } from 'vue'
-import { useRoute } from 'vue-router'
+import { computed, onMounted, onBeforeUnmount, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { ChatDotRound, Coin } from '@element-plus/icons-vue'
 import Header from './components/header.vue'
 import CreateEnterpriseDialog from './components/CreateEnterpriseDialog.vue'
+import PayQrCodeDialog from './components/PayQrCodeDialog.vue'
+import { detail } from '/@/api/frontend/home'
+import { createOrder, createQrCode, check } from '/@/api/frontend/order'
+import { checkCompany } from '/@/api/frontend/me'
 
 defineOptions({ name: 'ClientOrder' })
 
 const route = useRoute()
+const router = useRouter()
 
 const goodsId = ref(null)
-const goodsInfo = reactive({
-  name: '惊鸿·日卡',
-  price: '25',
-  sold: 318,
-  stockText: '即将售罄',
-  stock: 5,
-  deliveryType: '自动激活',
-  cover: 'https://picsum.photos/900/400',
-  type: 2, // 1: 个人 2: 企业
-  cardType: 4, // 1: 日卡 2: 周卡 3: 月卡 4: 季卡 5: 年卡 , 6: 按次数 , 7: 按积分
-  comboPeopleNum: 5, // 默认席位
-  comboPrice: 5, // 每席位单价
-  detailLines: [
-    '该卡可续杯 2 次',
-    '该卡可绑定一台设备',
-    '未激活前永久有效',
-    '激活后自下单起 7 日内有效',
-    '下单激活后不予退款,请确认好需要再下单',
-    '在 Iron 工具上使用视为激活',
-    '使用教程:请参考商家提供的使用说明链接'
-  ]
-})
+const goodsInfo = ref({})
 
 const buyCount = ref(1)
+const seatCount = ref(2)
 const hasEnterprise = ref(false)
 const createEnterpriseVisible = ref(false)
-const selectedPayMethod = ref('alipay')
-
-const isEnterprise = computed(() => Number(goodsInfo.type) === 2)
+const selectedPayMethod = ref('wechat')
+
+// 支付相关状态
+const payDialogVisible = ref(false)
+const qrCodeLoading = ref(false)
+const qrCodeUrl = ref('')
+const payStatus = ref('') // '', 'success', 'failed'
+const currentOrderInfo = ref({})
+const payCheckTimer = ref(null)
+const payCheckCount = ref(0)
+const MAX_PAY_CHECK_COUNT = 100 // 最大轮询次数(约5分钟)
+
+const isEnterprise = computed(() => Number(goodsInfo.value?.type) === 2)
 const isSeatBasedEnterprise = computed(() => {
   if (!isEnterprise.value) return false
-  const cardType = Number(goodsInfo.cardType)
-  return ![6, 7].includes(cardType)
+  const cardType = Number(goodsInfo.value?.card_type)
+  // 企业套餐:次卡(1)、积分卡(2),其余按席位
+  return ![1, 2].includes(cardType)
 })
 
+// 获取卡类型文本
 const cardTypeText = computed(() => {
+  if (goodsInfo.value?.card_info?.name) {
+    const cardName = goodsInfo.value.card_info.name
+    const showValue = goodsInfo.value.card_info.show_value
+    return showValue ? `${cardName}(${showValue})` : cardName
+  }
+  // 兼容处理:如果没有card_info,使用映射
   const map = {
-    1: '日卡',
-    2: '周卡',
-    3: '月卡',
-    4: '季卡',
-    5: '年卡',
-    6: '按次数',
-    7: '按积分'
+    1: '次卡',
+    2: '按积分',
+    3: '日卡',
+    4: '周卡',
+    5: '季卡',
+    6: '月卡',
+    7: '年卡'
   }
-  const label = map[Number(goodsInfo.cardType)] || '未知类型'
-  return isEnterprise.value ? `企业·${label}` : `个人·${label}`
+  const label = map[Number(goodsInfo.value?.card_type)] || '未知类型'
+  return label
 })
 
 const displayPrice = computed(() => {
   if (isSeatBasedEnterprise.value) {
-    return (Number(goodsInfo.comboPrice) * Number(goodsInfo.comboPeopleNum)).toFixed(2)
+    return (Number(goodsInfo.value?.price || 0) * seatCount.value).toFixed(2)
   }
-  return (Number(goodsInfo.price) * buyCount.value).toFixed(2)
+  return (Number(goodsInfo.value?.price || 0) * buyCount.value).toFixed(2)
 })
 
+// 获取商品详情
 const getGoodsInfo = () => {
   goodsId.value = route.params?.goodsId
+  detail({ id: goodsId.value }).then(res => {
+    goodsInfo.value = res.data
+  })
+}
+
+// 创建订单并生成支付二维码
+const createOrderAndQrCode = async () => {
+  try {
+    // 1. 创建订单
+    const quantity = isSeatBasedEnterprise.value ? seatCount.value : buyCount.value
+    const orderData = {
+      package_id: goodsId.value,
+      quantity: quantity
+    }
+
+    qrCodeLoading.value = true
+    const orderRes = await createOrder(orderData)
+
+    // 验证订单创建结果
+    if (!orderRes || !orderRes.data) {
+      ElMessage.error(orderRes?.msg || '创建订单失败,请重试')
+      qrCodeLoading.value = false
+      return
+    }
+
+    if (!orderRes.data.order_id || !orderRes.data.order_no) {
+      ElMessage.error(orderRes.data.msg || '订单信息不完整,请重试')
+      qrCodeLoading.value = false
+      return
+    }
+
+    const orderId = orderRes.data.order_id
+    // 根据实际返回的数据结构构建订单信息
+    currentOrderInfo.value = {
+      id: orderId,
+      order_id: orderId,
+      order_no: orderRes.data.order_no,
+      title: orderRes.data.package_name || goodsInfo.value.name || '',
+      product_name: orderRes.data.package_name || goodsInfo.value.name || '',
+      package_name: orderRes.data.package_name || '',
+      amount: orderRes.data.total_amount || displayPrice.value,
+      total_price: orderRes.data.total_amount || displayPrice.value,
+      total_amount: orderRes.data.total_amount || 0,
+      quantity: orderRes.data.quantity || (isSeatBasedEnterprise.value ? seatCount.value : buyCount.value),
+      ...orderRes.data
+    }
+
+    // 2. 生成支付二维码
+    const qrCodeData = {
+      order_id: orderId,
+      pay_method: selectedPayMethod.value
+    }
+
+    const qrCodeRes = await createQrCode(qrCodeData)
+
+    // 验证二维码生成结果
+    if (!qrCodeRes || !qrCodeRes.data) {
+      ElMessage.error(qrCodeRes?.msg || '生成支付二维码失败,请重试')
+      qrCodeLoading.value = false
+      return
+    }
+
+    // 根据接口返回数据结构,使用 qr_code_url 或 qr_code_content
+    const qrCodeValue = qrCodeRes.data.qr_code_url || qrCodeRes.data.qr_code_content
+    if (!qrCodeValue) {
+      ElMessage.error(qrCodeRes.data.msg || '二维码生成失败,请重试')
+      qrCodeLoading.value = false
+      return
+    }
+
+    // 更新订单信息,使用接口返回的数据
+    if (qrCodeRes.data.order_no) {
+      currentOrderInfo.value.order_no = qrCodeRes.data.order_no
+    }
+    if (qrCodeRes.data.pay_amount) {
+      currentOrderInfo.value.total_amount = qrCodeRes.data.pay_amount
+      currentOrderInfo.value.amount = qrCodeRes.data.pay_amount
+      currentOrderInfo.value.total_price = qrCodeRes.data.pay_amount
+    }
+    if (qrCodeRes.data.payment_id) {
+      currentOrderInfo.value.payment_id = qrCodeRes.data.payment_id
+    }
+    if (qrCodeRes.data.expire_time) {
+      currentOrderInfo.value.expire_time = qrCodeRes.data.expire_time
+    }
+
+    qrCodeUrl.value = qrCodeValue
+    qrCodeLoading.value = false
+    payStatus.value = ''
+
+    // 3. 显示支付弹窗
+    payDialogVisible.value = true
+
+    // 4. 开始轮询支付状态(使用接口返回的 order_no,如果没有则使用原来的)
+    const orderNo = qrCodeRes.data.order_no || orderRes.data.order_no
+    startPayStatusPolling(orderId, orderNo)
+  } catch (error) {
+    console.error('下单失败:', error)
+    ElMessage.error(error?.message || '下单失败,请重试')
+    qrCodeLoading.value = false
+  }
 }
 
-const placePersonalOrder = () => {
-  ElMessage.success(`已下单 ${buyCount.value} 件个人套餐`)
+// 开始轮询支付状态
+const startPayStatusPolling = (orderId, orderNo) => {
+  // 清除之前的定时器
+  if (payCheckTimer.value) {
+    clearInterval(payCheckTimer.value)
+  }
+
+  // 重置轮询计数
+  payCheckCount.value = 0
+
+  // 每3秒查询一次支付状态
+  payCheckTimer.value = setInterval(async () => {
+    try {
+      payCheckCount.value++
+
+      // 超过最大轮询次数,停止轮询
+      if (payCheckCount.value > MAX_PAY_CHECK_COUNT) {
+        stopPayStatusPolling()
+        ElMessage.warning('支付超时,请稍后手动查询订单状态')
+        return
+      }
+
+      // 根据接口注释,同时传递 order_id 和 order_no
+      const checkData = {
+        order_id: orderId,
+        order_no: orderNo
+      }
+      const res = await check(checkData)
+
+      //支付状态:0待支付,1已支付,2支付失败
+      const rawStatus = res.data?.pay_status
+
+      if (rawStatus == '1') {
+        // 支付成功
+        payStatus.value = 'success'
+        stopPayStatusPolling()
+
+        // 更新订单信息(可能包含最新的支付信息)
+        if (res.data) {
+          currentOrderInfo.value = {
+            ...currentOrderInfo.value,
+            ...res.data,
+            pay_status: rawStatus
+          }
+        }
+
+        // 3秒后自动关闭弹窗并跳转成功页
+        setTimeout(() => {
+          handlePaySuccess()
+        }, 3000)
+      } else if (rawStatus == '2' || rawStatus == '3') {
+        // 支付失败或已取消
+        payStatus.value = 'failed'
+        stopPayStatusPolling()
+        ElMessage.warning('订单已取消或支付失败')
+      }
+      // status === 0 或其他值表示待支付,继续轮询
+      // status === 0 或其他值表示待支付,继续轮询
+    } catch (error) {
+      // 查询失败不中断轮询,继续尝试(除非超过最大次数)
+      if (payCheckCount.value > MAX_PAY_CHECK_COUNT) {
+        stopPayStatusPolling()
+
+        const orderId = currentOrderInfo.value.order_id || currentOrderInfo.value.id
+        const orderNo = currentOrderInfo.value.order_no
+
+        if (orderId || orderNo) {
+          router.push({
+            name: 'userSuccess',
+            query: {
+              order_id: orderId,
+              order_no: orderNo
+            }
+          })
+        }
+      }
+    }
+  }, 3000)
 }
 
-const placeEnterpriseOrder = () => {
-  if (isSeatBasedEnterprise.value) {
-    ElMessage.success(`已为企业下单 ${goodsInfo.comboPeopleNum} 个席位`)
-  } else {
-    ElMessage.success(`已为企业下单 ${buyCount.value} 件套餐`)
+// 停止支付状态轮询
+const stopPayStatusPolling = () => {
+  if (payCheckTimer.value) {
+    clearInterval(payCheckTimer.value)
+    payCheckTimer.value = null
+  }
+  payCheckCount.value = 0
+}
+
+// 支付弹窗关闭处理
+const handlePayDialogClose = () => {
+  stopPayStatusPolling()
+  payDialogVisible.value = false
+  qrCodeUrl.value = ''
+  payStatus.value = ''
+  currentOrderInfo.value = {}
+  qrCodeLoading.value = false
+}
+
+// 支付二维码倒计时结束处理
+const handlePayExpired = () => {
+  stopPayStatusPolling()
+  payDialogVisible.value = false
+
+  const orderId = currentOrderInfo.value.order_id || currentOrderInfo.value.id
+  const orderNo = currentOrderInfo.value.order_no
+
+  if (orderId || orderNo) {
+    router.push({
+      name: 'userSuccess',
+      query: {
+        order_id: orderId,
+        order_no: orderNo
+      }
+    })
+  }
+
+  qrCodeUrl.value = ''
+  payStatus.value = ''
+  currentOrderInfo.value = {}
+  qrCodeLoading.value = false
+}
+
+// 支付成功处理
+const handlePaySuccess = () => {
+  stopPayStatusPolling()
+  payDialogVisible.value = false
+  ElMessage.success('支付成功!')
+
+  // 使用订单详情接口需要的参数跳转到成功页
+  const orderId = currentOrderInfo.value.order_id
+  const orderNo = currentOrderInfo.value.order_no
+
+  if (orderId || orderNo) {
+    router.push({
+      name: 'userSuccess',
+      query: {
+        order_id: orderId,
+        order_no: orderNo
+      }
+    })
   }
+
+  // 重置状态
+  qrCodeUrl.value = ''
+  payStatus.value = ''
+  currentOrderInfo.value = {}
 }
 
 const openCreateEnterpriseDialog = () => {
   createEnterpriseVisible.value = true
 }
 
-const handleEnterpriseCreated = () => {
-  hasEnterprise.value = true
+const handleEnterpriseCreated = async () => {
+  // 企业创建成功后,重新检查企业状态
+  await checkUserCompany()
+  ElMessage.success('企业创建成功!')
 }
 
 const handleSubmit = () => {
+  // 验证商品信息
+  if (!goodsId.value || !goodsInfo.value.id) {
+    ElMessage.warning('商品信息加载中,请稍候...')
+    return
+  }
+
+  // 验证库存
+  if (!isSeatBasedEnterprise.value && buyCount.value > goodsInfo.value.stock) {
+    ElMessage.warning('库存不足,请减少购买数量')
+    return
+  }
+
+  // 企业订单需要先创建企业
   if (isEnterprise.value) {
     if (!hasEnterprise.value) {
       ElMessage.warning('请先创建企业后再下单')
       openCreateEnterpriseDialog()
       return
     }
-    placeEnterpriseOrder()
-  } else {
-    placePersonalOrder()
+  }
+
+  // 创建订单并生成支付二维码
+  createOrderAndQrCode()
+}
+
+// 检查用户是否拥有企业
+const checkUserCompany = async () => {
+  try {
+    const res = await checkCompany()
+    hasEnterprise.value = res.data.has_company
+  } catch (error) {
+    hasEnterprise.value = false
   }
 }
 
 onMounted(() => {
   getGoodsInfo()
+  checkUserCompany()
+})
+
+// 组件卸载前清除定时器
+onBeforeUnmount(() => {
+  stopPayStatusPolling()
 })
 </script>
 
@@ -310,6 +550,10 @@ onMounted(() => {
     border-radius: 14px;
     overflow: hidden;
     background: #f2f6fc;
+    min-height: 200px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
 
     img {
       width: 100%;
@@ -317,6 +561,11 @@ onMounted(() => {
       object-fit: cover;
       display: block;
     }
+
+    .no-image {
+      color: #909399;
+      font-size: 14px;
+    }
   }
 }
 

+ 83 - 85
web/src/views/frontend/Register.vue

@@ -10,13 +10,8 @@
           已有账号?<router-link to="/user/login">立即登录</router-link>
         </div>
       </div>
-      <el-form
-        ref="registerFormRef"
-        :model="registerForm"
-        :rules="registerRules"
-        label-position="top"
-        class="client-register__form"
-      >
+      <el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-position="top"
+        class="client-register__form">
         <el-form-item label="联系人姓名" prop="name">
           <el-input v-model.trim="registerForm.name" placeholder="请输入联系人姓名" clearable />
         </el-form-item>
@@ -27,19 +22,10 @@
 
         <el-form-item label="邮箱验证码" prop="emailCode">
           <div class="client-register__code-row">
-            <el-input
-              class="client-register__code-input"
-              v-model.trim="registerForm.emailCode"
-              placeholder="请输入6位验证码"
-              maxlength="6"
-            />
-            <el-button
-              class="client-register__code-btn"
-              type="primary"
-              :loading="sendingCode"
-              :disabled="codeDisabled"
-              @click="handleSendEmailCode"
-            >
+            <el-input class="client-register__code-input" v-model.trim="registerForm.emailCode" placeholder="请输入6位验证码"
+              maxlength="6" />
+            <el-button class="client-register__code-btn" type="primary" :loading="sendingCode" :disabled="codeDisabled"
+              @click="handleSendEmailCode">
               {{ codeButtonText }}
             </el-button>
           </div>
@@ -54,7 +40,7 @@
         </el-form-item>
 
         <el-form-item label="邀请码(选填)" prop="inviteCode">
-          <el-input v-model.trim="registerForm.inviteCode" placeholder="没有可留空" clearable />
+          <el-input :disabled="userRegister" v-model.trim="registerForm.inviteCode" placeholder="没有可留空" clearable />
         </el-form-item>
 
         <el-form-item>
@@ -63,7 +49,7 @@
           </el-button>
         </el-form-item>
         <p class="client-register__agreement">
-          @2019-2025 成都聚德鼎盛文化传播有限公司 蜀ICP备19030658号-1
+          @2019-2026 成都聚德鼎盛文化传播有限公司 蜀ICP备19030658号-1
         </p>
       </el-form>
     </div>
@@ -71,8 +57,8 @@
 </template>
 
 <script setup>
-import { computed, onBeforeUnmount, reactive, ref } from 'vue'
-import { useRouter } from 'vue-router'
+import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { checkIn } from '/@/api/frontend/user/index'
 import { sendEms } from '/@/api/common'
@@ -83,7 +69,10 @@ import { useUserInfo } from '/@/stores/userInfo'
 defineOptions({ name: 'ClientRegister' })
 
 const router = useRouter()
+const route = useRoute()
 const userInfo = useUserInfo()
+// 邀请码开关
+const userRegister = ref(false)
 
 const registerFormRef = ref(null)
 const registerForm = reactive({
@@ -201,7 +190,7 @@ const handleSendEmailCode = () => {
     ElMessage.warning('请输入邮箱地址')
     return
   }
-  
+
   registerFormRef.value.validateField('email').then((valid) => {
     if (!valid) {
       // 验证失败,Element Plus 会自动显示错误信息
@@ -229,6 +218,15 @@ const handleSendEmailCode = () => {
   })
 }
 
+// 组件挂载时检查 URL 中的邀请码参数
+onMounted(() => {
+  const inviteCode = route.query.invite
+  if (inviteCode && typeof inviteCode === 'string') {
+    registerForm.inviteCode = inviteCode.trim()
+    userRegister.value = true
+  }
+})
+
 onBeforeUnmount(() => {
   clearCountdown()
 })
@@ -236,116 +234,116 @@ onBeforeUnmount(() => {
 
 <style scoped>
 .client-register {
-	min-height: 100vh;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	padding: 48px 16px;
-	background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 48px 16px;
+  background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
 }
 
 .client-register__panel {
-	width: 520px;
-	max-width: 100%;
-	padding: 28px 36px 24px;
-	background: #fff;
-	border-radius: 16px;
-	box-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
+  width: 520px;
+  max-width: 100%;
+  padding: 28px 36px 24px;
+  background: #fff;
+  border-radius: 16px;
+  box-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
 }
 
 .client-register__header {
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
-	margin-bottom: 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
 }
 
 .client-register__header h2 {
-	margin: 0;
-	font-size: 22px;
-	color: #303133;
+  margin: 0;
+  font-size: 22px;
+  color: #303133;
 }
 
 .client-register__header p {
-	margin: 6px 0 0;
-	color: #909399;
+  margin: 6px 0 0;
+  color: #909399;
 }
 
 .client-register__header-link {
-	font-size: 14px;
-	color: #606266;
+  font-size: 14px;
+  color: #606266;
 }
 
 .client-register__header-link a {
-	color: #409eff;
-	margin-left: 4px;
+  color: #409eff;
+  margin-left: 4px;
 }
 
 .client-register__form ::v-deep .el-form-item__label {
-	font-weight: 500;
-	color: #303133;
+  font-weight: 500;
+  color: #303133;
 }
 
 .client-register__form :deep(.el-form-item) {
-	margin-bottom: 18px;
+  margin-bottom: 18px;
 }
 
 .client-register__form :deep(.el-input) {
-	width: 100%;
+  width: 100%;
 }
 
 .client-register__code-row {
-	display: flex;
-	align-items: stretch;
-	width: 100%;
+  display: flex;
+  align-items: stretch;
+  width: 100%;
 }
 
 .client-register__code-input {
-	flex: 1;
+  flex: 1;
 }
 
 .client-register__code-btn {
-	white-space: nowrap;
-	margin-left: -1px;
-	min-width: 128px;
-	border-radius: 0 8px 8px 0;
+  white-space: nowrap;
+  margin-left: -1px;
+  min-width: 128px;
+  border-radius: 0 8px 8px 0;
 }
 
 .client-register__code-input :deep(.el-input__wrapper) {
-	border-radius: 8px 0 0 8px;
+  border-radius: 8px 0 0 8px;
 }
 
 .client-register__submit {
-	width: 100%;
-	height: 44px;
-	font-size: 15px;
+  width: 100%;
+  height: 44px;
+  font-size: 15px;
 }
 
 .client-register__agreement {
-	margin: -8px 0 0;
-	font-size: 12px;
-	color: #909399;
-	text-align: center;
+  margin: -8px 0 0;
+  font-size: 12px;
+  color: #909399;
+  text-align: center;
 }
 
 .client-register__agreement a {
-	color: #409eff;
-	text-decoration: none;
+  color: #409eff;
+  text-decoration: none;
 }
 
 @media (max-width: 600px) {
-	.client-register {
-		padding: 24px 12px;
-	}
-
-	.client-register__panel {
-		padding: 24px 20px;
-	}
-
-	.client-register__header {
-		flex-direction: column;
-		align-items: flex-start;
-		gap: 6px;
-	}
+  .client-register {
+    padding: 24px 12px;
+  }
+
+  .client-register__panel {
+    padding: 24px 20px;
+  }
+
+  .client-register__header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 6px;
+  }
 }
 </style>

+ 57 - 57
web/src/views/frontend/Reset.vue

@@ -11,32 +11,18 @@
         </div>
       </div>
 
-      <el-form
-        ref="resetFormRef"
-        :model="resetForm"
-        :rules="resetRules"
-        label-position="top"
-        class="client-reset__form"
-      >
+      <el-form ref="resetFormRef" :model="resetForm" :rules="resetRules" label-position="top"
+        class="client-reset__form">
         <el-form-item label="注册邮箱" prop="email">
           <el-input v-model.trim="resetForm.email" placeholder="请输入注册邮箱" clearable />
         </el-form-item>
 
-        <el-form-item label="邮箱验证码" prop="emailCode">
+        <el-form-item label="邮箱验证码" prop="captcha">
           <div class="client-reset__code-row">
-            <el-input
-              class="client-reset__code-input"
-              v-model.trim="resetForm.emailCode"
-              placeholder="请输入6位验证码"
-              maxlength="6"
-            />
-            <el-button
-              class="client-reset__code-btn"
-              type="primary"
-              :loading="sendingCode"
-              :disabled="codeDisabled"
-              @click="handleSendEmailCode"
-            >
+            <el-input class="client-reset__code-input" v-model.trim="resetForm.captcha" placeholder="请输入6位验证码"
+              maxlength="6" />
+            <el-button class="client-reset__code-btn" type="primary" :loading="sendingCode" :disabled="codeDisabled"
+              @click="handleSendEmailCode">
               {{ codeButtonText }}
             </el-button>
           </div>
@@ -56,7 +42,7 @@
           </el-button>
         </el-form-item>
         <p class="client-reset__tips">
-          @2019-2025 成都聚德鼎盛文化传播有限公司 蜀ICP备19030658号-1
+          @2019-2026 成都聚德鼎盛文化传播有限公司 蜀ICP备19030658号-1
         </p>
       </el-form>
     </div>
@@ -67,6 +53,10 @@
 import { computed, onBeforeUnmount, reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import { retrievePassword } from '/@/api/frontend/user/index'
+import { sendEms } from '/@/api/common'
+import clickCaptcha from '/@/components/clickCaptcha'
+import { uuid } from '/@/utils/random'
 
 defineOptions({ name: 'ClientReset' })
 
@@ -75,9 +65,10 @@ const router = useRouter()
 const resetFormRef = ref(null)
 const resetForm = reactive({
   email: '',
-  emailCode: '',
+  captcha: '',
   password: '',
-  confirmPassword: ''
+  confirmPassword: '',
+  captchaId: uuid()
 })
 
 const validateConfirmPassword = (rule, value, callback) => {
@@ -95,7 +86,7 @@ const resetRules = reactive({
     { required: true, message: '请输入注册邮箱', trigger: 'blur' },
     { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
   ],
-  emailCode: [
+  captcha: [
     { required: true, message: '请输入验证码', trigger: 'blur' },
     { min: 6, max: 6, message: '请输入6位验证码', trigger: 'blur' }
   ],
@@ -143,10 +134,14 @@ const handleReset = () => {
     if (!valid) return
     submitting.value = true
     try {
-      // TODO: 接入后端重置密码接口
-      await new Promise(resolve => setTimeout(resolve, 600))
+      await retrievePassword({
+        type: 'email',
+        account: resetForm.email.trim(),
+        captcha: resetForm.captcha,
+        password: resetForm.password
+      })
       ElMessage.success('密码已重置,请使用新密码登录')
-      router.push({ path: '/user/login', query: { email: resetForm.email.trim() }})
+      router.push({ path: '/user/login', query: { email: resetForm.email.trim() } })
     } catch (error) {
       ElMessage.error(error?.message || '重置失败,请稍后重试')
     } finally {
@@ -162,26 +157,32 @@ const handleSendEmailCode = () => {
     ElMessage.warning('请输入邮箱地址')
     return
   }
-  
-  resetFormRef.value.validateField('email').then(async (valid) => {
-    if (!valid) {
-      // 验证失败,Element Plus 会自动显示错误信息
-      return
-    }
-    sendingCode.value = true
-    try {
-      // TODO: 接入后端发送重置验证码接口
-      await new Promise(resolve => setTimeout(resolve, 500))
-      ElMessage.success('验证码发送成功,请查收邮箱')
-      startCountdown(60)
-    } catch (err) {
-      ElMessage.error(err?.message || '验证码发送失败')
-    } finally {
-      sendingCode.value = false
-    }
+
+  resetFormRef.value.validateField('email').then((valid) => {
+    if (!valid) return
+    // 先进行点击验证码验证
+    clickCaptcha(resetForm.captchaId, (captchaInfo) => {
+      sendEmailCaptcha(captchaInfo)
+    })
   })
 }
 
+const sendEmailCaptcha = (captchaInfo) => {
+  sendingCode.value = true
+  sendEms(resetForm.email.trim(), 'user_retrieve_pwd', {
+    captchaId: resetForm.captchaId,
+    captchaInfo: captchaInfo
+  })
+    .then((res) => {
+      if (res.code == 1) {
+        startCountdown(60)
+      }
+    })
+    .finally(() => {
+      sendingCode.value = false
+    })
+}
+
 onBeforeUnmount(() => {
   clearCountdown()
 })
@@ -240,32 +241,32 @@ onBeforeUnmount(() => {
 }
 
 .client-reset__form :deep(.el-form-item) {
-	margin-bottom: 18px;
+  margin-bottom: 18px;
 }
 
 .client-reset__form :deep(.el-input) {
-	width: 100%;
+  width: 100%;
 }
 
 .client-reset__code-row {
-	display: flex;
-	align-items: stretch;
-	width: 100%;
+  display: flex;
+  align-items: stretch;
+  width: 100%;
 }
 
 .client-reset__code-input {
-	flex: 1;
+  flex: 1;
 }
 
 .client-reset__code-btn {
-	white-space: nowrap;
-	margin-left: -1px;
-	min-width: 128px;
-	border-radius: 0 8px 8px 0;
+  white-space: nowrap;
+  margin-left: -1px;
+  min-width: 128px;
+  border-radius: 0 8px 8px 0;
 }
 
 .client-reset__code-input :deep(.el-input__wrapper) {
-	border-radius: 8px 0 0 8px;
+  border-radius: 8px 0 0 8px;
 }
 
 .client-reset__submit {
@@ -298,4 +299,3 @@ onBeforeUnmount(() => {
   }
 }
 </style>
-

+ 55 - 89
web/src/views/frontend/components/AddMemberDialog.vue

@@ -1,43 +1,20 @@
 <template>
-  <el-dialog
-    title="邮箱添加人员"
-    v-model:visible="dialogVisible"
-    width="480px"
-    :close-on-click-modal="false"
-    :close-on-press-escape="true"
-    class="add-member-dialog"
-    @close="handleClose"
-  >
+  <el-dialog title="邮箱添加人员" :model-value="props.visible" @update:model-value="(val) => emit('update:visible', val)"
+    width="480px" :close-on-click-modal="false" :close-on-press-escape="true" class="add-member-dialog"
+    @close="handleClose">
     <div class="add-member-form">
       <div class="form-item">
-        <div class="label">
-          <el-icon class="label-icon">
-            <Message />
-          </el-icon>
-          <span>邮箱地址</span>
-        </div>
         <div class="input-with-button">
-          <el-input
-            v-model="form.email"
-            placeholder="请输入邮箱地址"
-            clearable
-            :disabled="searching"
-            @keyup.enter="handleSearch"
-            @clear="handleClear"
-          >
+          <el-input v-model="form.email" placeholder="请输入邮箱地址" clearable :disabled="searching"
+            @keyup.enter="handleSearch" @clear="handleClear">
             <template #prefix>
               <el-icon class="input-prefix">
                 <Message />
               </el-icon>
             </template>
           </el-input>
-          <el-button
-            type="primary"
-            :loading="searching"
-            :disabled="!form.email || !form.email.trim()"
-            class="search-btn"
-            @click="handleSearch"
-          >
+          <el-button type="primary" :loading="searching" :disabled="!form.email || !form.email.trim()"
+            class="search-btn" @click="handleSearch">
             <el-icon v-if="!searching" class="search-icon">
               <Search />
             </el-icon>
@@ -95,17 +72,8 @@
 
     <template #footer>
       <span class="dialog-footer">
-        <el-button size="medium" @click="handleCancel">取消</el-button>
-        <el-button
-          type="primary"
-          size="medium"
-          :loading="submitting"
-          :disabled="!searchedUser"
-          @click="handleSubmit"
-        >
-        <el-icon v-if="!submitting" class="submit-icon">
-          <Check />
-        </el-icon>
+        <el-button size="small" @click="handleCancel">取消</el-button>
+        <el-button type="primary" size="small" :loading="submitting" :disabled="!searchedUser" @click="handleSubmit">
           <span>{{ submitting ? '添加中...' : '确认添加' }}</span>
         </el-button>
       </span>
@@ -114,8 +82,10 @@
 </template>
 
 <script setup>
-import { computed, getCurrentInstance, reactive, ref, watch } from 'vue'
-import { Check, Message, Search, SuccessFilled, User, UserFilled, WarningFilled } from '@element-plus/icons-vue'
+import { computed, reactive, ref, watch } from 'vue'
+import { Message, Search, SuccessFilled, User, UserFilled, WarningFilled } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { searchEmail, addMember } from '/@/api/frontend/me'
 
 defineOptions({ name: 'AddMemberDialog' })
 
@@ -131,7 +101,6 @@ const props = defineProps({
 })
 
 const emit = defineEmits(['update:visible', 'success', 'close'])
-const { proxy } = getCurrentInstance()
 
 const form = reactive({
   email: ''
@@ -140,11 +109,6 @@ const searchedUser = ref(null)
 const searching = ref(false)
 const submitting = ref(false)
 
-const dialogVisible = computed({
-  get: () => props.visible,
-  set: val => emit('update:visible', val)
-})
-
 const isValidEmail = computed(() => {
   if (!form.email || !form.email.trim()) {
     return true
@@ -156,14 +120,14 @@ const isValidEmail = computed(() => {
 watch(
   () => props.visible,
   newVal => {
-    if (!newVal) {
+    if (newVal) {
+      resetForm()
+    } else {
       resetForm()
     }
   }
 )
 
-const message = proxy?.$message
-
 const resetForm = () => {
   form.email = ''
   searchedUser.value = null
@@ -177,22 +141,25 @@ const handleClear = () => {
 
 const handleSearch = async () => {
   if (!form.email || !form.email.trim()) {
-    message?.warning('请输入邮箱')
+    ElMessage.warning('请输入邮箱')
     return
   }
 
   if (!isValidEmail.value) {
-    message?.error('邮箱格式不正确')
+    ElMessage.error('邮箱格式不正确')
     return
   }
 
   searching.value = true
   try {
-    message?.info('搜索功能待实现,需要调用搜索用户接口')
-    searchedUser.value = null
+    const response = await searchEmail({
+      keyword: form.email.trim()
+    })
+
+    searchedUser.value = response.data[0]
+    ElMessage.success('搜索成功')
   } catch (error) {
-    console.error('搜索用户失败:', error)
-    message?.error('搜索用户失败')
+    ElMessage.error('搜索用户失败,请稍后重试')
     searchedUser.value = null
   } finally {
     searching.value = false
@@ -201,30 +168,43 @@ const handleSearch = async () => {
 
 const handleSubmit = async () => {
   if (!searchedUser.value) {
-    message?.warning('请先搜索用户')
+    ElMessage.warning('请先搜索用户')
     return
   }
 
   if (!props.companyId) {
-    message?.error('企业ID不能为空')
+    ElMessage.error('企业ID不能为空')
+    return
+  }
+
+  if (!form.email || !form.email.trim()) {
+    ElMessage.error('邮箱不能为空')
     return
   }
 
+  // 获取用户姓名,如果搜索返回的数据中没有name,使用email作为默认值
+  const userName = searchedUser.value.name || searchedUser.value.email || form.email.trim()
+
   submitting.value = true
   try {
-    message?.info('添加成员功能待实现,需要调用添加成员接口')
+    await addMember({
+      company_id: String(props.companyId),
+      email: form.email.trim(),
+      name: userName
+    })
+
+    ElMessage.success('添加成员成功')
     emit('success', searchedUser.value)
-    dialogVisible.value = false
+    emit('update:visible', false)
   } catch (error) {
-    console.error('添加成员失败:', error)
-    message?.error('添加成员失败')
+    ElMessage.error('添加成员失败,请稍后重试')
   } finally {
     submitting.value = false
   }
 }
 
 const handleCancel = () => {
-  dialogVisible.value = false
+  emit('update:visible', false)
 }
 
 const handleClose = () => {
@@ -235,6 +215,7 @@ const handleClose = () => {
 
 <style scoped lang="scss">
 ::v-deep .add-member-dialog {
+
   .label-icon,
   .input-prefix,
   .search-icon,
@@ -270,7 +251,7 @@ const handleClose = () => {
 
   .el-dialog__header {
     padding: 20px 24px;
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    background: linear-gradient(135deg, #409eff 0%, #2563eb 100%);
     color: #fff;
     border-bottom: none;
 
@@ -327,7 +308,7 @@ const handleClose = () => {
     i {
       margin-right: 6px;
       font-size: 16px;
-      color: #667eea;
+      color: #409eff;
     }
 
     span {
@@ -347,22 +328,7 @@ const handleClose = () => {
         height: 40px;
         line-height: 40px;
         border-radius: 6px;
-        border: 1px solid #dcdfe6;
         transition: all 0.3s ease;
-
-        &:focus {
-          border-color: #667eea;
-          box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
-        }
-      }
-
-      ::v-deep .el-input__prefix {
-        left: 12px;
-        color: #909399;
-      }
-
-      ::v-deep .el-input__inner {
-        padding-left: 36px;
       }
     }
 
@@ -373,11 +339,11 @@ const handleClose = () => {
       border-radius: 6px;
       font-weight: 500;
       transition: all 0.3s ease;
-      box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
+      box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
 
       &:hover:not(:disabled) {
         transform: translateY(-1px);
-        box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
+        box-shadow: 0 4px 8px rgba(64, 158, 255, 0.3);
       }
 
       &:active:not(:disabled) {
@@ -422,13 +388,13 @@ const handleClose = () => {
       width: 48px;
       height: 48px;
       border-radius: 50%;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      background: linear-gradient(135deg, #409eff 0%, #2563eb 100%);
       display: flex;
       align-items: center;
       justify-content: center;
       margin-right: 16px;
       flex-shrink: 0;
-      box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+      box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
 
       i {
         font-size: 24px;
@@ -534,11 +500,11 @@ const handleClose = () => {
     transition: all 0.3s ease;
 
     &.el-button--primary {
-      box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
+      box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
 
       &:hover:not(:disabled) {
         transform: translateY(-1px);
-        box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
+        box-shadow: 0 4px 8px rgba(64, 158, 255, 0.3);
       }
 
       &:active:not(:disabled) {
@@ -558,10 +524,10 @@ const handleClose = () => {
     transform: scale(0);
     opacity: 0;
   }
+
   to {
     transform: scale(1);
     opacity: 1;
   }
 }
 </style>
-

+ 34 - 49
web/src/views/frontend/components/CreateEnterpriseDialog.vue

@@ -1,26 +1,15 @@
 <template>
-  <el-dialog
-    title="创建企业"
-    v-model:visible="dialogVisible"
-    width="420px"
-    @close="handleClose"
-  >
+  <el-dialog :model-value="props.visible" @update:model-value="(val) => emit('update:visible', val)" title="创建企业"
+    width="420px" @close="handleClose">
     <div class="enterprise-form">
       <div class="form-item">
         <div class="label">当前账号</div>
-        <el-input
-          v-model="form.account"
-          disabled
-        />
+        <el-input v-model="form.account" disabled />
         <p class="hint">注:一个账号只能归属一个企业</p>
       </div>
       <div class="form-item">
         <div class="label required">企业名称</div>
-        <el-input
-          v-model="form.name"
-          placeholder="请输入企业名称"
-          @keyup.enter="handleSubmit"
-        />
+        <el-input v-model="form.name" placeholder="请输入企业名称" @keyup.enter="handleSubmit" />
       </div>
     </div>
     <template #footer>
@@ -33,7 +22,10 @@
 </template>
 
 <script setup>
-import { computed, getCurrentInstance, reactive, ref, watch } from 'vue'
+import { getCurrentInstance, reactive, ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import { createCompany } from '/@/api/frontend/me'
+import { useUserInfo } from '/@/stores/userInfo'
 
 defineOptions({ name: 'CreateEnterpriseDialog' })
 
@@ -44,12 +36,10 @@ const props = defineProps({
   }
 })
 
-const emit = defineEmits(['update:visible', 'close'])
-const { proxy } = getCurrentInstance()
-// mock client user info to decouple from vuex
-const mockClientUser = reactive({
-  email: 'demo_client@example.com'
-})
+const emit = defineEmits(['update:visible', 'close', 'success'])
+import { ElMessage } from 'element-plus'
+const userStore = useUserInfo()
+const { email } = storeToRefs(userStore)
 
 const form = reactive({
   account: '',
@@ -57,13 +47,6 @@ const form = reactive({
 })
 const submitting = ref(false)
 
-const dialogVisible = computed({
-  get: () => props.visible,
-  set: val => emit('update:visible', val)
-})
-
-const clientEmail = computed(() => mockClientUser.email || '')
-
 watch(
   () => props.visible,
   newVal => {
@@ -76,18 +59,6 @@ watch(
   }
 )
 
-watch(
-  clientEmail,
-  newVal => {
-    if (newVal && !form.account) {
-      form.account = newVal
-    }
-  },
-  { immediate: true }
-)
-
-const message = proxy?.$message
-
 const initForm = () => {
   form.name = ''
   ensureClientEmail()
@@ -99,24 +70,23 @@ const resetForm = () => {
 }
 
 const ensureClientEmail = () => {
-  // use mock email as placeholder until real API is wired
-  form.account = clientEmail.value || 'client@example.com'
+  form.account = email.value || '未知账号'
 }
 
 const validateForm = () => {
   if (!form.name || !form.name.trim()) {
-    message?.error('请输入企业名称')
+    ElMessage.error('请输入企业名称')
     return false
   }
 
   const trimmedName = form.name.trim()
   if (trimmedName.length < 2) {
-    message?.error('企业名称至少需要2个字符')
+    ElMessage.error('企业名称至少需要2个字符')
     return false
   }
 
   if (trimmedName.length > 50) {
-    message?.error('企业名称不能超过50个字符')
+    ElMessage.error('企业名称不能超过50个字符')
     return false
   }
 
@@ -131,11 +101,27 @@ const handleSubmit = async () => {
 }
 
 const createEnterprise = async () => {
-  // TODO: 接入创建企业接口
+  if (submitting.value) return
+
+  submitting.value = true
+  try {
+    const response = await createCompany({ name: form.name.trim() })
+    if (response.code === 1) {
+      ElMessage.success('企业创建成功')
+      emit('success', response.data)
+      emit('update:visible', false)
+    } else {
+      ElMessage.error(response.msg || '创建企业失败')
+    }
+  } catch (error) {
+    ElMessage.error('创建企业失败,请稍后重试')
+  } finally {
+    submitting.value = false
+  }
 }
 
 const handleCancel = () => {
-  dialogVisible.value = false
+  emit('update:visible', false)
 }
 
 const handleClose = () => {
@@ -173,4 +159,3 @@ const handleClose = () => {
   }
 }
 </style>
-

+ 160 - 168
web/src/views/frontend/components/Me/enterprise.vue

@@ -21,7 +21,7 @@
         </div>
         <p class="empty-text">您还没有加入任何企业</p>
 
-        <el-button type="primary" @click="showCreateDialog = true">
+        <el-button type="primary" @click="openCreateDialog">
           创建企业
         </el-button>
       </div>
@@ -46,47 +46,37 @@
           </div>
         </div>
 
+        <div style="margin-bottom: 10px;">
+          <el-button type="primary" size="small" plain @click="handleOpenProductSeatDialog">产品分配</el-button>
+        </div>
+
         <!-- 员工列表表格 -->
         <div class="table-section">
-          <el-table
-            ref="employeeTable"
-            :data="employeeList"
-            style="width: 100%"
-            :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }"
-            empty-text="暂无员工数据"
-            @selection-change="handleSelectionChange"
-          >
-            <el-table-column type="selection" width="55" align="center" />
+          <el-table ref="employeeTable" :data="employeeList" style="width: 100%"
+            :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }" empty-text="暂无员工数据"
+           >
             <el-table-column prop="id" label="编号" width="100" align="center" />
             <el-table-column prop="name" label="姓名" min-width="150" align="center" />
             <el-table-column prop="email" label="邮箱" min-width="200" align="center" />
-            <el-table-column prop="status" label="状态" width="120" align="center">
+            <el-table-column prop="role_text" label="角色" width="140" align="center">
               <template #default="{ row }">
-                <el-tag :type="getStatusType(row.status)" size="small">
-                  {{ getStatusText(row.status) }}
+                <el-tag :type="row.role === 1 ? 'success' : 'info'" size="small">
+                  {{ row.role_text }}
                 </el-tag>
               </template>
             </el-table-column>
-            <el-table-column prop="createdAt" label="注册时间" width="180" align="center">
+            <el-table-column prop="status" label="状态" width="120" align="center">
               <template #default="{ row }">
-                {{ formatDate(row.createdAt || row.created_time || row.createdTime) }}
+                <el-tag :type="getStatusType(row.status)" size="small">
+                  {{ row.status_text }}
+                </el-tag>
               </template>
             </el-table-column>
-            <el-table-column label="操作" width="200" align="center" fixed="right">
+            <el-table-column prop="join_time" label="加入时间" width="160" align="center" />
+            <el-table-column label="操作" width="100" align="center" fixed="right">
               <template #default="{ row }">
-                <el-button
-                  type="primary"
-                  size="small"
-                  @click="handleOpenProductSeatDialog([row.id])"
-                >
-                  产品分配
-                </el-button>
-                <el-button
-                  type="danger"
-                  size="small"
-                  :disabled="row.id === currentUserId"
-                  @click="handleDeleteMember(row)"
-                >
+                <el-button type="danger" size="small" :disabled="!row.can_operate || row.id === currentUserId"
+                  @click="handleDeleteMember(row)">
                   删除
                 </el-button>
               </template>
@@ -94,69 +84,27 @@
           </el-table>
         </div>
 
-        <!-- 批量操作工具栏 -->
-        <div v-if="selectedEmployeeIds.length > 0" class="batch-actions">
-          <div class="batch-info">
-            <span>已选择 <strong>{{ selectedEmployeeIds.length }}</strong> 位员工</span>
-          </div>
-          <div class="batch-buttons">
-            <el-button
-              type="primary"
-              size="small"
-              :icon="Goods"
-              @click="handleOpenProductSeatDialog(selectedEmployeeIds)"
-            >
-              批量产品分配
-            </el-button>
-            <el-button
-              size="small"
-              :icon="Close"
-              @click="handleClearSelection"
-            >
-              取消选择
-            </el-button>
-          </div>
-        </div>
-
         <!-- 分页控件 -->
         <div v-if="employeeList.length > 0 || total > 0" class="pagination-section">
-          <el-pagination
-            :current-page="currentPage"
-            :page-size="pageSize"
-            :page-sizes="pageSizes"
-            :total="total"
-            layout="total, sizes, prev, pager, next, jumper"
-            background
-            @size-change="handlePageSizeChange"
-            @current-change="handleCurrentPageChange"
-          />
+          <el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="pageSizes" :total="total"
+            layout="total, sizes, prev, pager, next, jumper" background @size-change="handlePageSizeChange"
+            @current-change="handleCurrentPageChange" />
         </div>
       </div>
     </div>
 
     <!-- 创建企业弹框 -->
-    <CreateEnterpriseDialog
-      v-model:visible="showCreateDialog"
-      @success="handleCreateSuccess"
-      @close="handleDialogClose"
-    />
+    <CreateEnterpriseDialog v-model:visible="showCreateDialog" @success="handleCreateSuccess"
+      @close="handleDialogClose" />
 
     <!-- 添加企业成员弹框 -->
-    <AddMemberDialog
-      v-model:visible="showAddMemberDialog"
-      :company-id="currentCompanyId"
-      @success="handleAddMemberSuccess"
-      @close="handleAddMemberDialogClose"
-    />
+    <AddMemberDialog v-model:visible="showAddMemberDialog" :company-id="currentCompanyId" @success="loadEnterpriseList"
+      @close="showAddMemberDialog = false" />
 
     <!-- 产品分配席位弹框 -->
-    <ProductSeatDialog
-      v-model:visible="showProductSeatDialog"
-      :member-list="employeeList"
-      :selected-member-ids="selectedMemberIdsForDialog"
-      @success="handleProductSeatSuccess"
-      @close="handleProductSeatDialogClose"
-    />
+    <ProductSeatDialog v-model:visible="showProductSeatDialog" :member-list="employeeList"
+      :selected-member-ids="selectedMemberIdsForDialog" @success="handleProductSeatSuccess"
+      @close="handleProductSeatDialogClose" />
   </div>
 </template>
 
@@ -164,22 +112,28 @@
 import { computed, onMounted, ref, watch } from 'vue'
 import { storeToRefs } from 'pinia'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Close, Goods, Loading, OfficeBuilding, Plus } from '@element-plus/icons-vue'
+import {  Loading, OfficeBuilding, Plus } from '@element-plus/icons-vue'
 import { useUserInfo } from '/@/stores/userInfo'
 import CreateEnterpriseDialog from '../CreateEnterpriseDialog.vue'
 import AddMemberDialog from '../AddMemberDialog.vue'
 import ProductSeatDialog from '../ProductSeatDialog.vue'
+import { checkCompany, myCompanies, members, removeMember } from '/@/api/frontend/me'
 
 defineOptions({ name: 'MeEnterprise' })
 
 type Employee = {
   id: number
-  name: string
-  email: string
-  status: number
-  createdAt: string
-  created_time?: string
-  createdTime?: string
+  name: string // 企业名称
+  email: string // 电子邮箱
+  role: number // 角色:1创建人/管理员,2普通成员
+  status: number // 状态:1正常,2禁用
+  join_time: string // 加入时间
+  create_time: string // 创建时间
+  role_text: string // 角色文本
+  status_text: string // 状态文本
+  join_time_formatted: string // 格式化的加入时间
+  create_time_formatted: string // 格式化的创建时间
+  can_operate: boolean // 是否可以操作(只有创建人可以操作)
 }
 
 type ProductSeatResult = {
@@ -192,25 +146,17 @@ const { id: userId } = storeToRefs(userStore)
 
 const loading = ref(false)
 const employeeList = ref<Employee[]>([
-  { id: 1, name: '张三', email: 'zhangsan@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
-  { id: 2, name: '李四', email: 'lisi@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
-  { id: 3, name: '王五', email: 'wangwu@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
-  { id: 4, name: '赵六', email: 'zhaoliu@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
-  { id: 5, name: '孙七', email: 'sunqi@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
-  { id: 6, name: '周八', email: 'zhouba@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
-  { id: 7, name: '吴九', email: 'wujiu@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
-  { id: 8, name: '郑十', email: 'zhengshi@example.com', status: 1, createdAt: '2025-01-01 12:00:00' }
+
 ])
 const showCreateDialog = ref(false)
 const showAddMemberDialog = ref(false)
 const showProductSeatDialog = ref(false)
-const selectedEmployeeIds = ref<number[]>([])
 const selectedMemberIdsForDialog = ref<number[]>([])
-const employeeTable = ref()
+
 const currentPage = ref(1)
 const pageSize = ref(10)
 const pageSizes = [10, 20, 30, 50]
-const total = ref(1)
+const total = ref(0)
 
 // 企业数据占位,后续可通过接口填充
 const companyId = ref<number | string | null>(null)
@@ -223,7 +169,7 @@ const hasEnterprise = computed(() => {
   return true
 })
 
-const currentCompanyId = computed(() => companyId.value)
+const currentCompanyId: any = computed(() => companyId.value)
 
 const currentEnterpriseName = computed(() => {
   if (!hasEnterprise.value) return '我的企业'
@@ -233,46 +179,48 @@ const currentEnterpriseName = computed(() => {
 
 const currentUserId = computed(() => userId.value || null)
 
-const formatDate = (dateStr?: string) => {
-  if (!dateStr) return '-'
-  const date = new Date(dateStr)
-  if (Number.isNaN(date.getTime())) return dateStr
-
-  const year = date.getFullYear()
-  const month = String(date.getMonth() + 1).padStart(2, '0')
-  const day = String(date.getDate()).padStart(2, '0')
-  const hours = String(date.getHours()).padStart(2, '0')
-  const minutes = String(date.getMinutes()).padStart(2, '0')
-
-  return `${year}-${month}-${day} ${hours}:${minutes}`
-}
+// 请求锁,防止重复请求
+const isLoadingList = ref(false)
 
-type TagType = 'success' | 'warning' | 'info' | 'danger' | 'primary'
-
-const getStatusType = (status: number): TagType => {
-  const statusMap: Record<number, TagType> = {
-    1: 'success',
-    2: 'danger'
+// 获取企业列表及员工
+const loadEnterpriseList = async () => {
+  if (!hasEnterprise.value) {
+    loading.value = false
+    employeeList.value = []
+    total.value = 0
+    return
   }
-  return statusMap[status] || 'info'
-}
 
-const getStatusText = (status: number) => {
-  const statusMap: Record<number, string> = {
-    1: '启用',
-    2: '禁用'
+  // 如果正在加载,直接返回,避免重复请求
+  if (isLoadingList.value) {
+    return
   }
-  return statusMap[status] || '未知'
-}
 
-const loadEnterpriseList = async () => {
-  // TODO: 接入实际接口
-  loading.value = false
-}
+  isLoadingList.value = true
+  loading.value = true
+  try {
+    // 获取企业成员列表
+    const membersRes = await members({
+      company_id: companyId.value,
+      page: currentPage.value,
+      pageSize: pageSize.value
+    })
 
-const initEnterpriseData = async () => {
-  // TODO: 拉取企业数据并填充 companyId/companyName
-  loadEnterpriseList()
+    employeeList.value = membersRes.data?.list || []
+    total.value = membersRes.data?.total || 0
+  } catch (error: any) {
+    // 如果是取消的请求(axios 自动取消重复请求),不显示错误
+    // axios 取消的请求通常会有 message 属性包含 'cancel' 或 'Cancel'
+    if (error?.message && (error.message.includes('cancel') || error.message.includes('Cancel'))) {
+      return
+    }
+    ElMessage.error('获取成员列表失败')
+    employeeList.value = []
+    total.value = 0
+  } finally {
+    loading.value = false
+    isLoadingList.value = false
+  }
 }
 
 const handlePageSizeChange = (val: number) => {
@@ -286,66 +234,94 @@ const handleCurrentPageChange = (val: number) => {
   loadEnterpriseList()
 }
 
-const handleCreateSuccess = async () => {
-  // TODO: 创建成功后刷新企业信息
-  currentPage.value = 1
-  loadEnterpriseList()
-  ElMessage.success('企业创建成功')
+const handleCreateSuccess = async (data?: any) => {
+  // 创建成功后刷新企业信息
+  if (data?.id) {
+    currentPage.value = 1
+    companyId.value = data.id
+    companyName.value = data.name || ''
+    // companyId 变化会触发 watch,自动加载列表,不需要手动调用
+  } else {
+    // 如果没有返回数据,重新检查企业信息
+    await handleCheckCompany()
+  }
 }
 
 const handleDialogClose = () => {
   showCreateDialog.value = false
 }
 
+// 创建企业
+const openCreateDialog = () => {
+  console.log('openCreateDialog called, showCreateDialog before:', showCreateDialog.value)
+  showCreateDialog.value = true
+  console.log('showCreateDialog after:', showCreateDialog.value)
+}
+
 const handleDeleteMember = async (member: Employee) => {
   if (!member?.id) {
     ElMessage.error('成员信息不完整')
     return
   }
 
+  if (!member.can_operate) {
+    ElMessage.warning('您没有权限操作此成员')
+    return
+  }
+
   try {
     await ElMessageBox.confirm(`确定要删除成员 "${member.name || member.email}" 吗?`, '提示', {
       confirmButtonText: '确定',
       cancelButtonText: '取消',
       type: 'warning'
     })
-    // TODO: 调用删除成员 API
+
+    await removeMember({
+      member_id: member.id,
+      company_id: companyId.value
+    })
+
     ElMessage.success('删除成员成功')
     loadEnterpriseList()
-  } catch {
-    // 用户取消
+  } catch (error: any) {
+    ElMessage.error('删除成员失败')
   }
 }
 
-const handleAddMemberSuccess = () => {
-  loadEnterpriseList()
-}
-
-const handleAddMemberDialogClose = () => {
-  showAddMemberDialog.value = false
-}
-
-const handleSelectionChange = (selection: Employee[]) => {
-  selectedEmployeeIds.value = selection.map(item => item.id)
-}
-
-const handleClearSelection = () => {
-  employeeTable.value?.clearSelection()
-  selectedEmployeeIds.value = []
+// 检查是否有企业
+const handleCheckCompany = async () => {
+  try {
+    const res = await checkCompany()
+    if (res.code === 1 && res.data?.has_company) {
+      // 如果已有企业,获取企业信息
+      const companiesRes = await myCompanies({})
+      const company = companiesRes.data
+      currentPage.value = 1
+      companyId.value = company.id
+      companyName.value = company.name || ''
+      // companyId 变化会触发 watch,自动加载列表,不需要手动调用
+    } else {
+      // 没有企业,清空数据
+      companyId.value = null
+      companyName.value = ''
+    }
+  } catch (error) {
+    console.error('检查企业失败:', error)
+    ElMessage.error('检查企业状态失败')
+    companyId.value = null
+    companyName.value = ''
+  }
 }
 
-const handleOpenProductSeatDialog = (memberIds: number[]) => {
-  if (!memberIds || memberIds.length === 0) {
-    ElMessage.warning('请至少选择一个员工')
-    return
-  }
-  selectedMemberIdsForDialog.value = memberIds
+// 选择分配员工
+const handleOpenProductSeatDialog = () => {
+  // 直接打开分配弹窗,不再依赖表格选中
+  selectedMemberIdsForDialog.value = []
   showProductSeatDialog.value = true
 }
 
 const handleProductSeatSuccess = (data: ProductSeatResult) => {
   loadEnterpriseList()
-  handleClearSelection()
   const count = data?.memberIds?.length || 0
   ElMessage.success(
     data.mode === 'assign'
@@ -359,18 +335,33 @@ const handleProductSeatDialogClose = () => {
   selectedMemberIdsForDialog.value = []
 }
 
+type TagType = 'success' | 'warning' | 'info' | 'danger' | 'primary'
+
+const getStatusType = (status: number): TagType => {
+  const statusMap: Record<number, TagType> = {
+    1: 'success',
+    2: 'danger'
+  }
+  return statusMap[status] || 'info'
+}
+
 watch(
   companyId,
   (newVal, oldVal) => {
-    if (newVal && !oldVal) {
+    // 只在 companyId 从无到有或发生变化时加载
+    // 避免初始化时 immediate 触发(因为初始值为 null)
+    if (newVal && newVal !== oldVal) {
       loadEnterpriseList()
+    } else if (!newVal && oldVal) {
+      // 如果企业被清空,清空列表
+      employeeList.value = []
+      total.value = 0
     }
-  },
-  { immediate: true }
+  }
 )
 
 onMounted(() => {
-  initEnterpriseData()
+  handleCheckCompany()
 })
 </script>
 
@@ -589,6 +580,7 @@ onMounted(() => {
   0% {
     transform: rotate(0deg);
   }
+
   100% {
     transform: rotate(360deg);
   }

+ 678 - 46
web/src/views/frontend/components/Me/info.vue

@@ -10,56 +10,193 @@
       <p class="welcome-text">欢迎回来, {{ userName }}</p>
     </div>
 
-    <!-- 财务卡片 -->
-    <div class="finance-cards">
-      <div class="finance-card">
-        <div class="card-icon">
-          <el-icon class="card-icon__inner">
-            <Medal />
-          </el-icon>
+   
+
+    <!-- 佣金详情区域 -->
+    <div class="commission-section">
+      <h3 class="section-title">
+        <el-icon class="title-icon"><TrendCharts /></el-icon>
+        佣金详情
+      </h3>
+      <div class="commission-grid">
+        <div class="commission-card">
+          <div class="commission-icon icon-total">
+            <el-icon><Medal /></el-icon>
+          </div>
+          <div class="commission-content">
+            <div class="commission-label">总佣金</div>
+            <div class="commission-value">¥{{ formatMoney(uersInfo?.commission_info?.total_commission) }}</div>
+            <div class="commission-desc">累计获得的所有佣金</div>
+          </div>
         </div>
-        <div class="card-content">
-          <div class="card-label">总收入</div>
-          <div class="card-value">¥{{ commission_total || '0' }}</div>
+        <div class="commission-card" @click="gotoWithdraw">
+          <div class="commission-icon icon-current">
+            <el-icon><Wallet /></el-icon>
+          </div>
+          <div class="commission-content" style="cursor: pointer;">
+            <div class="commission-label">当前佣金</div>
+            <div class="commission-value">¥{{ formatMoney(uersInfo?.commission_info?.current_commission) }} <span>去提现</span></div>
+            <div class="commission-desc">可提现的佣金余额</div>
+          </div>
+        </div>
+        <div class="commission-card">
+          <div class="commission-icon icon-withdrawn">
+            <el-icon><Money /></el-icon>
+          </div>
+          <div class="commission-content">
+            <div class="commission-label">已提现佣金</div>
+            <div class="commission-value">¥{{ formatMoney(uersInfo?.commission_info?.withdrawn_commission) }}</div>
+            <div class="commission-desc">已经成功提现的金额</div>
+          </div>
+        </div>
+        <div class="commission-card">
+          <div class="commission-icon icon-frozen">
+            <el-icon><Lock /></el-icon>
+          </div>
+          <div class="commission-content">
+            <div class="commission-label">冻结中佣金</div>
+            <div class="commission-value">¥{{ formatMoney(uersInfo?.commission_info?.frozen_commission) }}</div>
+            <div class="commission-desc">暂时无法提现的佣金</div>
+          </div>
+        </div>
+        <div class="commission-card">
+          <div class="commission-icon icon-today">
+            <el-icon><Calendar /></el-icon>
+          </div>
+          <div class="commission-content">
+            <div class="commission-label">今日佣金</div>
+            <div class="commission-value">¥{{ formatMoney(uersInfo?.commission_info?.today_income) }}</div>
+            <div class="commission-desc">今天获得的佣金收入</div>
+          </div>
+        </div>
+        <div class="commission-card">
+          <div class="commission-icon icon-month">
+            <el-icon><TrendCharts /></el-icon>
+          </div>
+          <div class="commission-content">
+            <div class="commission-label">当月佣金</div>
+            <div class="commission-value">¥{{ formatMoney(uersInfo?.commission_info?.month_income) }}</div>
+            <div class="commission-desc">本月累计获得的佣金</div>
+          </div>
         </div>
       </div>
-      <div class="finance-card">
-        <div class="card-icon">
-          <el-icon class="card-icon__inner">
-            <Coin />
-          </el-icon>
+    </div>
+
+    <!-- 邀请信息 -->
+    <div class="promotion-section">
+      <h3 class="section-title">
+        <el-icon class="title-icon"><User /></el-icon>
+        邀请信息
+      </h3>
+      
+      <!-- 邀请统计卡片 -->
+      <div class="promotion-stats">
+        <div class="stat-card stat-primary">
+          <div class="stat-icon">
+            <el-icon><Avatar /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-label">邀请总人数</div>
+            <div class="stat-value">{{ uersInfo?.promotion_info?.total_invitation || 0 }}</div>
+            <div class="stat-desc">累计邀请的总人数</div>
+          </div>
+        </div>
+        <div class="stat-card stat-success">
+          <div class="stat-icon">
+            <el-icon><Calendar /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-label">今日邀请人数</div>
+            <div class="stat-value">{{ uersInfo?.promotion_info?.invitation_count || 0 }}</div>
+            <div class="stat-desc">今天新增的邀请人数</div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 邀请码卡片 -->
+      <div class="promotion-card">
+        <div class="promotion-card-header">
+          <div class="promotion-card-title">
+            <el-icon class="title-icon-small"><Ticket /></el-icon>
+            <span>我的邀请码</span>
+          </div>
         </div>
-        <div class="card-content">
-          <div class="card-label">余额</div>
-          <div class="card-value">¥{{ commission_amount || '0' }}</div>
+        <div class="promotion-card-body">
+          <div class="code-display">
+            <span class="code-text">{{ uersInfo?.promotion_info?.invitation_code || '-' }}</span>
+            <el-button 
+              type="primary" 
+              :icon="DocumentCopy" 
+              size="small" 
+              circle
+              @click="copyInvitationCode"
+              class="copy-btn"
+            />
+          </div>
+          <div class="code-desc">分享此邀请码给好友,好友注册后您将获得丰厚佣金</div>
         </div>
       </div>
-      <div class="finance-card">
-        <div class="card-icon">
-          <el-icon class="card-icon__inner">
-            <Avatar />
-          </el-icon>
+
+      <!-- 邀请链接卡片 -->
+      <div class="promotion-card">
+        <div class="promotion-card-header">
+          <div class="promotion-card-title">
+            <el-icon class="title-icon-small"><Link /></el-icon>
+            <span>我的邀请链接</span>
+          </div>
         </div>
-        <div class="card-content">
-          <div class="card-label">推广人数</div>
-          <div class="card-value">{{ promoters || '0' }}</div>
+        <div class="promotion-card-body">
+          <div class="link-display">
+            <div class="link-text">{{ uersInfo?.promotion_info?.promotion_link || '-' }}</div>
+            <el-button 
+              type="primary" 
+              :icon="DocumentCopy" 
+              size="small" 
+              circle
+              @click="copyInvitationLink"
+              class="copy-btn"
+            />
+          </div>
+          <div class="link-desc">通过该链接注册的用户,其消费将为您带来丰厚佣金</div>
         </div>
       </div>
     </div>
 
-    <!-- 商户信息 -->
-    <div class="info-section">
-      <h3 class="section-title">商户信息</h3>
-      <div class="info-grid">
-        <div class="info-item">
-          <span class="info-label">商户ID:</span>
-          <span class="info-value merchant-id">{{ user_id || '-' }}</span>
+    <!-- 登录信息 -->
+    <div class="login-section">
+      <h3 class="section-title">
+        <el-icon class="title-icon"><Monitor /></el-icon>
+        登录信息
+      </h3>
+      <div class="login-grid">
+        <div class="login-card">
+          <div class="login-icon icon-current">
+            <el-icon><Clock /></el-icon>
+          </div>
+          <div class="login-content">
+            <div class="login-label">本次登录时间</div>
+            <div class="login-value">{{ uersInfo?.login_info?.current_login_time || '-' }}</div>
+            <div class="login-desc">您当前会话的登录时间</div>
+          </div>
         </div>
-        <div class="info-item full-width">
-          <span class="info-label">推广链接:</span>
-          <div class="promotion-link-wrapper">
-            <span>{{ promotion_link || '-' }}</span>
-            <span class="promotion-desc">(通过该链接消费的人会给您丰厚的佣金)</span>
+        <div class="login-card">
+          <div class="login-icon icon-last">
+            <el-icon><Clock /></el-icon>
+          </div>
+          <div class="login-content">
+            <div class="login-label">上次登录时间</div>
+            <div class="login-value">{{ uersInfo?.login_info?.last_login_time || '-' }}</div>
+            <div class="login-desc">您上一次登录的时间</div>
+          </div>
+        </div>
+        <div class="login-card">
+          <div class="login-icon icon-ip">
+            <el-icon><Connection /></el-icon>
+          </div>
+          <div class="login-content">
+            <div class="login-label">登录IP地址</div>
+            <div class="login-value">{{ uersInfo?.login_info?.join_ip || '-' }}</div>
+            <div class="login-desc">您当前登录的IP地址</div>
           </div>
         </div>
       </div>
@@ -69,26 +206,87 @@
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, ref } from 'vue'
-import { Avatar, Coin, Medal } from '@element-plus/icons-vue'
+import { computed, onMounted, ref,} from 'vue'
+import { Avatar, Coin, Medal, Wallet, Money, Lock, TrendCharts, Calendar, DocumentCopy, User, Link, Ticket, Clock, Monitor, Connection } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
 import { storeToRefs } from 'pinia'
 import { useUserInfo } from '/@/stores/userInfo'
+import { dashboard } from '/@/api/frontend/me'
+import { useRouter } from 'vue-router'
 
 defineOptions({ name: 'MeInfo' })
 
+const router = useRouter()
+
 const userStore = useUserInfo()
 const { username, email, id } = storeToRefs(userStore)
 
-const promoters = ref<string | number>(0)
-const promotion_link = ref('')
-const commission_total = ref<string | number>(0)
-const commission_amount = ref<string | number>(0)
-const user_id = computed(() => id.value || '-')
+// 用户信息
+const uersInfo = ref<anyObj>({})
 
 const userName = computed(() => username.value || email.value)
 
+// 格式化金额
+const formatMoney = (amount: number | undefined) => {
+  if (amount === undefined || amount === null) return '0.00'
+  return Number(amount).toFixed(2)
+}
+
+// 复制邀请码
+const copyInvitationCode = () => {
+  const code = uersInfo.value?.promotion_info?.invitation_code
+  if (!code) {
+    ElMessage.warning('邀请码不存在')
+    return
+  }
+  navigator.clipboard.writeText(code).then(() => {
+    ElMessage.success('邀请码已复制到剪贴板')
+  }).catch(() => {
+    // 降级方案
+    const textarea = document.createElement('textarea')
+    textarea.value = code
+    document.body.appendChild(textarea)
+    textarea.select()
+    document.execCommand('copy')
+    document.body.removeChild(textarea)
+    ElMessage.success('邀请码已复制到剪贴板')
+  })
+}
+
+// 复制邀请链接
+const copyInvitationLink = () => {
+  const link = uersInfo.value?.promotion_info?.promotion_link
+  if (!link) {
+    ElMessage.warning('邀请链接不存在')
+    return
+  }
+  navigator.clipboard.writeText(link).then(() => {
+    ElMessage.success('邀请链接已复制到剪贴板')
+  }).catch(() => {
+    // 降级方案
+    const textarea = document.createElement('textarea')
+    textarea.value = link
+    document.body.appendChild(textarea)
+    textarea.select()
+    document.execCommand('copy')
+    document.body.removeChild(textarea)
+    ElMessage.success('邀请链接已复制到剪贴板')
+  })
+}
+
+// 获取用户信息
 const getUserInfo = () => {
-  // TODO: 接入实际的用户信息获取逻辑
+  dashboard().then(res => {
+    uersInfo.value = res.data
+  })
+}
+
+//跳转提现页面
+const gotoWithdraw =()=>{
+  router.push({
+    path: '/user/me',
+    query: { tab: 'withdraw-list' }
+  })
 }
 
 onMounted(() => {
@@ -134,6 +332,197 @@ onMounted(() => {
       font-weight: 600;
       color: #303133;
     }
+  }
+
+  .promotion-section {
+    margin-top: 30px;
+    padding: 24px;
+    background: linear-gradient(135deg, #fafbfc 0%, #ffffff 100%);
+    border-radius: 12px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    .section-title {
+      display: flex;
+      align-items: center;
+      margin: 0 0 24px 0;
+      font-size: 20px;
+      font-weight: 600;
+      color: #303133;
+
+      .title-icon {
+        margin-right: 8px;
+        font-size: 22px;
+        color: #409EFF;
+      }
+    }
+
+    .promotion-stats {
+      display: grid;
+      grid-template-columns: repeat(2, 1fr);
+      gap: 16px;
+      margin-bottom: 24px;
+
+      @media (max-width: 768px) {
+        grid-template-columns: 1fr;
+      }
+
+      .stat-card {
+        display: flex;
+        align-items: center;
+        padding: 20px;
+        background: #ffffff;
+        border-radius: 10px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+        border: 1px solid #f0f0f0;
+        transition: all 0.3s ease;
+
+        &:hover {
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+          transform: translateY(-2px);
+        }
+
+        &.stat-primary {
+          border-left: 4px solid #409EFF;
+        }
+
+        &.stat-success {
+          border-left: 4px solid #67C23A;
+        }
+
+        .stat-icon {
+          width: 48px;
+          height: 48px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 10px;
+          margin-right: 16px;
+          flex-shrink: 0;
+          font-size: 24px;
+          color: #fff;
+        }
+
+        &.stat-primary .stat-icon {
+          background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+        }
+
+        &.stat-success .stat-icon {
+          background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
+        }
+
+        .stat-content {
+          flex: 1;
+
+          .stat-label {
+            font-size: 14px;
+            color: #909399;
+            margin-bottom: 6px;
+            font-weight: 500;
+          }
+
+          .stat-value {
+            font-size: 28px;
+            font-weight: 700;
+            color: #303133;
+            margin-bottom: 4px;
+          }
+
+          .stat-desc {
+            font-size: 12px;
+            color: #C0C4CC;
+            line-height: 1.4;
+          }
+        }
+      }
+    }
+
+    .promotion-card {
+      margin-bottom: 20px;
+      background: #ffffff;
+      border-radius: 10px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+      border: 1px solid #f0f0f0;
+      overflow: hidden;
+      transition: all 0.3s ease;
+
+      &:hover {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+      }
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .promotion-card-header {
+        padding: 16px 20px;
+        background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
+        border-bottom: 1px solid #f0f0f0;
+
+        .promotion-card-title {
+          display: flex;
+          align-items: center;
+          font-size: 16px;
+          font-weight: 600;
+          color: #303133;
+
+          .title-icon-small {
+            margin-right: 8px;
+            font-size: 18px;
+            color: #409EFF;
+          }
+        }
+      }
+
+      .promotion-card-body {
+        padding: 20px;
+
+        .code-display,
+        .link-display {
+          display: flex;
+          align-items: center;
+          gap: 12px;
+          margin-bottom: 12px;
+          padding: 12px 16px;
+          background: #f5f7fa;
+          border-radius: 8px;
+          border: 1px solid #e4e7ed;
+
+          .code-text {
+            flex: 1;
+            font-size: 18px;
+            font-weight: 600;
+            color: #409EFF;
+            letter-spacing: 2px;
+            font-family: 'Courier New', monospace;
+          }
+
+          .link-text {
+            flex: 1;
+            font-size: 14px;
+            color: #606266;
+            word-break: break-all;
+            line-height: 1.5;
+          }
+
+          .copy-btn {
+            flex-shrink: 0;
+            transition: all 0.3s ease;
+
+            &:hover {
+              transform: scale(1.1);
+            }
+          }
+        }
+
+        .code-desc,
+        .link-desc {
+          font-size: 13px;
+          color: #909399;
+          line-height: 1.6;
+          padding-left: 4px;
+        }
+      }
+    }
 
     .info-grid {
       display: grid;
@@ -160,6 +549,7 @@ onMounted(() => {
           margin-right: 10px;
           min-width: 100px;
           flex-shrink: 0;
+          text-align: right;
         }
 
         .info-value {
@@ -239,6 +629,18 @@ onMounted(() => {
         transform: translateY(-2px);
       }
 
+      &.card-primary .card-icon {
+        background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+      }
+
+      &.card-success .card-icon {
+        background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
+      }
+
+      &.card-info .card-icon {
+        background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%);
+      }
+
       .card-icon {
         width: 48px;
         height: 48px;
@@ -273,6 +675,236 @@ onMounted(() => {
       }
     }
   }
+
+  .commission-section {
+    margin-top: 30px;
+    padding: 24px;
+    background: linear-gradient(135deg, #fafbfc 0%, #ffffff 100%);
+    border-radius: 12px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    .section-title {
+      display: flex;
+      align-items: center;
+      margin: 0 0 24px 0;
+      font-size: 20px;
+      font-weight: 600;
+      color: #303133;
+
+      .title-icon {
+        margin-right: 8px;
+        font-size: 22px;
+        color: #409EFF;
+      }
+    }
+
+    .commission-grid {
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 20px;
+
+      @media (max-width: 1024px) {
+        grid-template-columns: repeat(2, 1fr);
+      }
+
+      @media (max-width: 768px) {
+        grid-template-columns: 1fr;
+        gap: 16px;
+      }
+
+      .commission-card {
+        display: flex;
+        align-items: flex-start;
+        padding: 20px;
+        background: #ffffff;
+        border-radius: 10px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+        transition: all 0.3s ease;
+        border: 1px solid #f0f0f0;
+
+        &:hover {
+          box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+          transform: translateY(-3px);
+          border-color: #e0e0e0;
+        }
+
+        .commission-icon {
+          width: 56px;
+          height: 56px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 12px;
+          margin-right: 16px;
+          flex-shrink: 0;
+          font-size: 28px;
+          color: #fff;
+
+          &.icon-total {
+            background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+          }
+
+          &.icon-current {
+            background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
+          }
+
+          &.icon-withdrawn {
+            background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
+          }
+
+          &.icon-frozen {
+            background: linear-gradient(135deg, #F56C6C 0%, #f78989 100%);
+          }
+
+          &.icon-today {
+            background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%);
+          }
+
+          &.icon-month {
+            background: linear-gradient(135deg, #9C27B0 0%, #ba68c8 100%);
+          }
+        }
+
+        .commission-content {
+          flex: 1;
+          min-width: 0;
+
+          .commission-label {
+            font-size: 14px;
+            color: #909399;
+            margin-bottom: 8px;
+            font-weight: 500;
+          }
+
+          .commission-value {
+            font-size: 24px;
+            font-weight: 700;
+            color: #303133;
+            margin-bottom: 6px;
+            word-break: break-all;
+
+            span{
+              color: #409EFF;
+              font-size: 10px;
+            }
+          }
+
+          .commission-desc {
+            font-size: 12px;
+            color: #C0C4CC;
+            line-height: 1.4;
+          }
+        }
+      }
+    }
+  }
+
+  .login-section {
+    margin-top: 30px;
+    padding: 24px;
+    background: linear-gradient(135deg, #fafbfc 0%, #ffffff 100%);
+    border-radius: 12px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    .section-title {
+      display: flex;
+      align-items: center;
+      margin: 0 0 24px 0;
+      font-size: 20px;
+      font-weight: 600;
+      color: #303133;
+
+      .title-icon {
+        margin-right: 8px;
+        font-size: 22px;
+        color: #409EFF;
+      }
+    }
+
+    .login-grid {
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 20px;
+
+      @media (max-width: 1024px) {
+        grid-template-columns: repeat(2, 1fr);
+      }
+
+      @media (max-width: 768px) {
+        grid-template-columns: 1fr;
+        gap: 16px;
+      }
+
+      .login-card {
+        display: flex;
+        align-items: flex-start;
+        padding: 18px;
+        background: #ffffff;
+        border-radius: 10px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+        transition: all 0.3s ease;
+        border: 1px solid #f0f0f0;
+
+        &:hover {
+          box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+          transform: translateY(-3px);
+          border-color: #e0e0e0;
+        }
+
+        .login-icon {
+          width: 56px;
+          height: 56px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 12px;
+          margin-right: 16px;
+          flex-shrink: 0;
+          font-size: 28px;
+          color: #fff;
+
+          &.icon-current {
+            background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+          }
+
+          &.icon-last {
+            background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
+          }
+
+          &.icon-ip {
+            background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
+          }
+        }
+
+        .login-content {
+          flex: 1;
+          min-width: 0;
+
+          .login-label {
+            font-size: 14px;
+            color: #909399;
+            margin-bottom: 8px;
+            font-weight: 500;
+          }
+
+          .login-value {
+            font-size: 16px;
+            font-weight: 600;
+            color: #303133;
+            margin-bottom: 6px;
+            word-break: break-all;
+            line-height: 1.5;
+          }
+
+          .login-desc {
+            font-size: 12px;
+            color: #C0C4CC;
+            line-height: 1.4;
+          }
+        }
+      }
+    }
+  }
 }
 </style>
 

+ 231 - 175
web/src/views/frontend/components/Me/purchaseHistory.vue

@@ -8,20 +8,13 @@
     </div>
 
     <div class="history-filters">
-      <el-input v-model="searchForm.orderId" placeholder="请输入订单号" clearable class="filter-item order-input">
+      <el-input v-model="searchForm.order_no" placeholder="请输入订单号" clearable class="filter-item order-input">
         <template #prepend>订单号</template>
       </el-input>
 
-      <el-date-picker
-        v-model="searchForm.dateRange"
-        class="filter-item date-picker"
-        type="daterange"
-        unlink-panels
-        start-placeholder="下单开始时间"
-        end-placeholder="下单结束时间"
-        range-separator="~"
-        value-format="yyyy-MM-dd"
-      />
+      <el-date-picker v-model="searchForm.dateRange" class="filter-item date-picker" type="daterange"
+                unlink-panels start-placeholder="开始时间" end-placeholder="结束时间" range-separator="~"
+                value-format="YYYY-MM-DD" style="width: 100px !important;" />
 
       <el-button size="small" type="primary" :icon="Search" @click="handleSearch">查询</el-button>
       <el-button size="small" @click="handleReset">重置</el-button>
@@ -29,219 +22,230 @@
 
     <div class="history-tabs">
       <el-tabs v-model="activeStatus" @tab-click="handleTabClick">
-        <el-tab-pane
-          v-for="tab in statusTabs"
-          :key="tab.value"
-          :label="`${tab.label} (${getOrderCount(tab.value)})`"
-          :name="tab.value"
-        />
+        <el-tab-pane v-for="tab in statusTabs" :key="tab.value" :label="`${tab.label} (${getOrderCount(tab.value)})`"
+          :name="tab.value" />
       </el-tabs>
     </div>
 
-    <div v-if="filteredOrders.length" class="order-list">
-      <div v-for="order in filteredOrders" :key="order.orderId" class="order-card">
-        <div class="order-card__header">
-          <div class="header-left">
-            <span class="order-id">订单号:{{ order.orderId }}</span>
-          </div>
-          <div class="header-right">
-            <span>下单时间:{{ order.orderTime }}</span>
-          </div>
-        </div>
-
-        <div class="order-card__body">
-          <div class="order-item product-info">
-            <div class="item-label">商品</div>
-            <div class="item-content">
-              <div class="product-name">{{ order.productName }}</div>
+    <div v-loading="loading" class="order-list-wrapper">
+      <div v-if="orders.length" class="order-list">
+        <div v-for="order in orders" :key="order.order_no" class="order-card">
+          <div class="order-card__header">
+            <div class="header-left">
+              <span class="order-id">订单号:{{ order.order_no }}</span>
+              <el-tag size="small" :type="order.pay_status_text === '已支付' ? 'success' : 'warning'" class="status-tag">
+                {{ order.pay_status_text }}
+              </el-tag>
+            </div>
+            <div class="header-right">
+              <span class="order-time">下单时间:{{ order.order_time_formatted }}</span>
             </div>
           </div>
 
-          <div v-if="order.type === 1" class="order-item">
-            <div class="item-label">数量</div>
-            <div class="item-value">x{{ order.quantity }}</div>
-          </div>
-          <div v-if="order.type === 2" class="order-item">
-            <div class="item-label">席位</div>
-            <div class="item-value">{{ order.quantity }}个</div>
-          </div>
+          <div class="order-card__body">
+            <div class="order-item product-info">
+              <div class="item-label">商品名称</div>
+              <div class="item-content">
+                <div class="product-name">{{ order.product_name }}</div>
+              </div>
+            </div>
 
-          <div class="order-item">
-            <div class="item-label">金额</div>
-            <div class="item-value amount">¥{{ order.amount }}</div>
-          </div>
+            <div class="order-item">
+              <div class="item-label">购买数量</div>
+              <div class="item-value">x{{ order.quantity }}</div>
+            </div>
 
-          <div class="order-item">
-            <div class="item-label">支付方式</div>
-            <div class="item-value">{{ order.payMethod }}</div>
-          </div>
+            <div class="order-item">
+              <div class="item-label">订单金额</div>
+              <div class="item-value amount">{{ order.amount }}</div>
+            </div>
 
-          <div class="order-item">
-            <div class="item-label">支付状态</div>
-            <el-tag size="small" :type="order.payStatus === '已支付' ? 'success' : 'warning'">
-              {{ order.payStatus }}
-            </el-tag>
-          </div>
+            <div class="order-item">
+              <div class="item-label">支付方式</div>
+              <div class="item-value pay-method">{{ order.pay_method_text }}</div>
+            </div>
 
-          <div class="order-item">
-            <div class="item-label">发货方式</div>
-            <div class="item-value">自动发货</div>
-          </div>
+            <div class="order-item">
+              <div class="item-label">发货方式</div>
+              <div class="item-value">自动开通</div>
+            </div>
 
-          <div class="order-item">
-            <div class="item-label">套餐类型</div>
-            <div class="item-value">{{ order.type === 1 ? '个人套餐' : '企业套餐' }}</div>
-          </div>
+            <div class="order-item">
+              <div class="item-label">付款时间</div>
+              <div class="item-value">{{ order.pay_time_formatted || '-' }}</div>
+            </div>
 
-          <div class="order-item full-width">
-            <div class="item-label">商家留言</div>
-            <div class="item-value note">
-              {{ order.merchantNote }}
+            <div class="order-item full-width">
+              <div class="item-label">商家留言</div>
+              <div class="item-value note">
+                请前往相对应的网站进行使用工具
+              </div>
             </div>
-          </div>
 
-          <div class="order-item full-width time-info">
-            <span>下单时间:{{ order.payTime || '-' }}</span>
-            <span>付款时间:{{ order.deliverTime || '-' }}</span>
+            <!-- <div class="order-item full-width time-info">
+              <div class="time-item">
+                <span class="time-label">付款时间:</span>
+                <span class="time-value">{{ order.pay_time_formatted || '-' }}</span>
+              </div>
+            </div> -->
           </div>
         </div>
       </div>
+
+      <el-empty v-else-if="!loading" class="history-empty" description="暂无符合条件的订单" />
     </div>
 
-    <el-empty v-else class="history-empty" description="暂无符合条件的订单" />
+    <!-- 分页组件 -->
+    <div v-if="pagination.total > 0" class="pagination-wrapper">
+      <el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
+        :page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange" @current-change="handleCurrentChange" />
+    </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { computed, reactive, ref } from 'vue'
+import { onMounted, reactive, ref } from 'vue'
 import { Search } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { purchaseRecordsSimple } from '/@/api/frontend/me'
+import dayjs from 'dayjs'
 
 defineOptions({ name: 'MePurchaseHistory' })
 
 type OrderRecordStatus = 'paid' | 'unpaid'
 type OrderTabStatus = 'all' | OrderRecordStatus
+
+// 新的订单数据结构,匹配接口返回
 type Order = {
-  orderId: string
-  productName: string
-  sku: string
+  order_no: string
+  product_name: string
   quantity: number
-  type: number
-  amount: number
-  payMethod: string
-  payStatus: string
-  merchantNote: string
-  codes: string[]
-  orderTime: string
-  payTime: string
-  deliverTime: string
-  status: OrderRecordStatus
+  amount: string
+  pay_method_text: string
+  pay_status_text: string
+  order_time_formatted: string
+  pay_time_formatted: string
+}
+
+// API 返回的数据结构
+type PurchaseRecordsResponse = {
+  list: Order[]
+  total: number
+  page: number
+  page_size: number
+  stats: {
+    total_orders: number
+    paid_orders: number
+    unpaid_orders: number
+  }
 }
 
 const activeStatus = ref<OrderTabStatus>('all')
-const searchForm = reactive<{ orderId: string; dateRange: string[] }>({
-  orderId: '',
-  dateRange: []
-})
-const appliedFilters = reactive<{ orderId: string; dateRange: string[] }>({
-  orderId: '',
-  dateRange: []
+const loading = ref(false)
+const searchForm = reactive<{
+  order_no: string
+  dateRange: string[]
+}>({
+  order_no: '',
+  dateRange:[dayjs().format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]
 })
 const statusTabs: { label: string; value: OrderTabStatus }[] = [
   { label: '全部', value: 'all' },
   { label: '已支付', value: 'paid' },
   { label: '未支付', value: 'unpaid' }
 ]
-const orders = ref<Order[]>([
-  {
-    orderId: '6952512020095823599',
-    productName: '望舒·月卡',
-    sku: '-',
-    quantity: 1,
-    type: 1,
-    amount: 52,
-    payMethod: '支付宝',
-    payStatus: '已支付',
-    merchantNote: '请前往相对应的网站进行使用工具,具体方法在文档里。激活前永久有效,激活后30天内有效。',
-    codes: ['XCCSYVIPJ7LM85R6IIOYXP7S6TO'],
-    orderTime: '2025-10-01 12:01:02',
-    payTime: '2025-10-01 12:01:02',
-    deliverTime: '2025-10-01 12:01:32',
-    status: 'paid'
-  },
-  {
-    orderId: '4502508151712325808',
-    productName: '清和·季卡',
-    sku: '-',
-    quantity: 1,
-    type: 2,
-    amount: 168,
-    payMethod: '微信',
-    payStatus: '待支付',
-    merchantNote: '如有疑问请联系在线客服,超过24小时自动关闭。',
-    codes: [],
-    orderTime: '2025-09-22 09:36:15',
-    payTime: '',
-    deliverTime: '',
-    status: 'unpaid'
-  },
-  {
-    orderId: '55665111144555',
-    productName: '聚德神笔日卡',
-    sku: 'JD-001-DAY',
-    quantity: 1,
-    type: 1,
-    amount: 6,
-    payMethod: '微信',
-    payStatus: '已支付',
-    merchantNote: '请前往相对应的网站进行使用工具,具体方法在文档里。激活前永久有效,激活后30天内有效。',
-    codes: ['JDSB20251001-0001'],
-    orderTime: '2025-10-01 12:01:02',
-    payTime: '2025-10-01 12:01:02',
-    deliverTime: '2025-10-01 12:01:05',
-    status: 'paid'
-  }
-])
-
-const filteredOrders = computed(() =>
-  orders.value.filter(order => {
-    const matchesStatus = activeStatus.value === 'all' ? true : order.status === activeStatus.value
-    const orderIdFilter = appliedFilters.orderId
-      ? order.orderId.includes(appliedFilters.orderId.trim())
-      : true
-    const dateFilter = isWithinRange(order.orderTime, appliedFilters.dateRange)
-    return matchesStatus && orderIdFilter && dateFilter
-  })
-)
+const orders = ref<Order[]>([])
+const stats = ref<PurchaseRecordsResponse['stats']>({
+  total_orders: 0,
+  paid_orders: 0,
+  unpaid_orders: 0
+})
+
+// 分页相关
+const pagination = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0
+})
+
+// 加载购买记录
+const loadPurchaseRecords = async () => {
+  try {
+    loading.value = true
+    const params: any = {
+      page: pagination.currentPage,
+      pageSize: pagination.pageSize,
+      status: activeStatus.value
+    }
 
+    // 添加搜索条件
+    if (searchForm.order_no) {
+      params.order_no = searchForm.order_no.trim()
+    }
+    if (searchForm.dateRange) {
+      params.star_time = searchForm.dateRange[0]
+      params.end_time = searchForm.dateRange[1]
+    }
+
+    const res = await purchaseRecordsSimple(params)
+    const data = res.data as PurchaseRecordsResponse
+    orders.value = data.list || []
+    stats.value = data.stats || { total_orders: 0, paid_orders: 0, unpaid_orders: 0 }
+    pagination.total = data.total || 0
+  } catch (error) {
+    ElMessage.error('加载购买记录失败,请稍后重试')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 搜索
 const handleSearch = () => {
-  appliedFilters.orderId = searchForm.orderId
-  appliedFilters.dateRange = [...searchForm.dateRange]
+  pagination.currentPage = 1 // 重置到第一页
+  loadPurchaseRecords()
 }
 
+// 重置
 const handleReset = () => {
-  searchForm.orderId = ''
-  searchForm.dateRange = []
-  appliedFilters.orderId = ''
-  appliedFilters.dateRange = []
+  searchForm.order_no = ''
+  searchForm.dateRange = [dayjs().format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]
+  pagination.currentPage = 1
+  loadPurchaseRecords()
 }
 
+// 标签页切换
 const handleTabClick = (tab: { paneName?: string | number; name?: string | number }) => {
   activeStatus.value = (tab.paneName || tab.name || 'all') as OrderTabStatus
+  pagination.currentPage = 1 // 切换状态时重置到第一页
+  loadPurchaseRecords()
 }
 
+// 获取订单数量(使用后端返回的统计数据)
 const getOrderCount = (status: OrderTabStatus) => {
-  if (status === 'all') return orders.value.length
-  return orders.value.filter(order => order.status === status).length
+  if (status == 'all') return stats.value.total_orders || 0
+  if (status == 'paid') return stats.value.paid_orders || 0
+  if (status == 'unpaid') return stats.value.unpaid_orders || 0
+  return 0
+}
+
+// 分页大小改变
+const handleSizeChange = (size: number) => {
+  pagination.pageSize = size
+  pagination.currentPage = 1
+  loadPurchaseRecords()
 }
 
-const isWithinRange = (orderTime: string, range: string[]) => {
-  if (!range || range.length !== 2) return true
-  const [start, end] = range
-  const startDate = new Date(`${start} 00:00:00`)
-  const endDate = new Date(`${end} 23:59:59`)
-  const targetDate = new Date(orderTime.replace(/-/g, '/'))
-  return targetDate >= startDate && targetDate <= endDate
+// 当前页改变
+const handleCurrentChange = (page: number) => {
+  pagination.currentPage = page
+  loadPurchaseRecords()
 }
+
+// 初始化加载数据
+onMounted(() => {
+  loadPurchaseRecords()
+})
 </script>
 
 <style scoped lang="scss">
@@ -280,6 +284,7 @@ const isWithinRange = (orderTime: string, range: string[]) => {
 .history-filters {
   display: flex;
   flex-wrap: wrap;
+  align-items: center;
   gap: 12px;
   padding: 16px;
   background: linear-gradient(135deg, #ecf5ff 0%, #f5f9ff 100%);
@@ -320,6 +325,10 @@ const isWithinRange = (orderTime: string, range: string[]) => {
   }
 }
 
+.order-list-wrapper {
+  min-height: 200px;
+}
+
 .order-list {
   display: flex;
   flex-direction: column;
@@ -343,10 +352,29 @@ const isWithinRange = (orderTime: string, range: string[]) => {
     background: linear-gradient(135deg, #ecf5ff, #ffffff);
   }
 
+  .header-left {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
   .order-id {
     font-weight: 600;
-    margin-right: 12px;
     color: #1f2d3d;
+    font-size: 15px;
+  }
+
+  .status-tag {
+    font-weight: 500;
+  }
+
+  .header-right {
+    color: #606266;
+    font-size: 13px;
+  }
+
+  .order-time {
+    white-space: nowrap;
   }
 
   .order-card__body {
@@ -391,6 +419,11 @@ const isWithinRange = (orderTime: string, range: string[]) => {
     font-weight: 600;
   }
 
+  .pay-method {
+    color: #606266;
+    font-weight: 500;
+  }
+
   .note {
     padding: 10px;
     background: #f0f5ff;
@@ -421,11 +454,26 @@ const isWithinRange = (orderTime: string, range: string[]) => {
   display: flex;
   justify-content: space-between;
   flex-wrap: wrap;
-  gap: 10px;
-  font-size: 13px;
-  color: #606266;
-  padding-top: 10px;
+  gap: 16px;
+  padding-top: 16px;
   border-top: 1px dashed #d9e2f0;
+  margin-top: 4px;
+
+  .time-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 13px;
+  }
+
+  .time-label {
+    color: #909399;
+  }
+
+  .time-value {
+    color: #606266;
+    font-weight: 500;
+  }
 }
 
 .history-empty {
@@ -435,6 +483,15 @@ const isWithinRange = (orderTime: string, range: string[]) => {
   border: 1px dashed #c6e2ff;
 }
 
+.pagination-wrapper {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
+}
+
 @media (max-width: 768px) {
   .history-header {
     flex-direction: column;
@@ -462,4 +519,3 @@ const isWithinRange = (orderTime: string, range: string[]) => {
   }
 }
 </style>
-

+ 459 - 93
web/src/views/frontend/components/Me/purchaseProduct.vue

@@ -1,29 +1,107 @@
 <template>
   <div class="me-purchase-product">
     <div class="product-header">
-      <h2 class="product-title">我的产品</h2>
+      <h2 class="product-title">我的产品({{ productsNum }})</h2>
     </div>
     <div class="product-content">
-      <div class="product-grid">
-        <div
-          v-for="item in products"
-          :key="item.id"
-          class="product-card"
-          @click="handleProductClick(item)"
-        >
+      <el-empty v-if="products.length == 0" description="暂无产品">
+        <el-button type="primary" @click="gotoBuy">去购买</el-button>
+      </el-empty>
+      <div v-else class="product-grid">
+        <div v-for="item in products" :key="item.usage_id" class="product-card" :class="{ 'expired': item.status != 1 }"
+          @click="handleProductClick(item)">
+          <!-- 状态标签 -->
+          <div class="status-badge" :class="getStatusClass(item.status)">
+            {{ getStatusText(item.status) }}
+          </div>
+
+          <!-- 产品图片 -->
           <div class="product-image-wrapper">
-            <img src="https://picsum.photos/200" alt="">
+            <img :src="item.image" :alt="item.product_name">
+            <div class="product-type-badge" :class="item.product_type === 1 ? 'web' : 'software'">
+              <el-icon v-if="item.product_type == 1">
+                <Link />
+              </el-icon>
+              <el-icon v-else>
+                <Monitor />
+              </el-icon>
+              <span>{{ item.product_type == 1 ? '网页' : '软件' }}</span>
+            </div>
           </div>
+
+          <!-- 产品信息 -->
           <div class="product-info">
-            <div class="product-name">{{ item.name }}</div>
-            <div class="product-desc">{{ item.description }}</div>
-            <div v-if="item.expireAt" class="product-expire">
-              <span class="expire-label">有效期到:</span>
-              <span class="expire-text">{{ item.expireAt }}</span>
+            <div class="product-main">
+              <!-- 左侧:产品基本信息 -->
+              <div class="product-main-left">
+                <div class="product-name" :title="item.product_name">{{ item.product_name }}</div>
+
+                <!-- 套餐信息 -->
+                <div class="package-info">
+                  <el-tag size="small" type="primary" effect="plain">{{ item.package_type_text }}</el-tag>
+                  <el-tag size="small" type="warning" effect="plain">{{ item.card_type_text }}</el-tag>
+                </div>
+
+                <!-- 使用情况 -->
+                <div class="usage-info">
+                  <!-- 次卡显示剩余次数 -->
+                  <div v-if="item.card_type == 1" class="usage-item">
+                    <el-icon>
+                      <Tickets />
+                    </el-icon>
+                    <span>剩余次数: <strong>{{ item.remaining_times }}</strong></span>
+                  </div>
+
+                  <!-- 积分卡显示剩余积分 -->
+                  <div v-if="item.card_type == 2" class="usage-item">
+                    <el-icon>
+                      <Coin />
+                    </el-icon>
+                    <span>剩余积分: <strong>{{ item.remaining_points }}</strong></span>
+                  </div>
+
+                  <!-- 日期卡显示到期时间 -->
+                  <div v-if="[3, 4, 5, 6, 7].includes(item.card_type)" class="usage-item">
+                    <el-icon>
+                      <Calendar />
+                    </el-icon>
+                    <span>{{ item.display_info }}</span>
+                  </div>
+                </div>
+              </div>
+
+              <!-- 右侧:席位信息(企业套餐)- 仅日期卡显示(次卡/积分卡不展示“已分配”相关信息) -->
+              <div
+                v-if="item.package_type == 2 && item.total_seats && [3, 4, 5, 6, 7].includes(item.card_type)"
+                class="product-main-right"
+              >
+                <div class="seats-info">
+                  <div class="seats-row">
+                    <div class="seats-label">席位</div>
+                    <div class="seats-value">
+                      <span class="seats-number strong">{{ item.assigned_seats || 0 }}</span>
+                      <span class="seats-split">/</span>
+                      <span class="seats-number">{{ item.total_seats }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
             </div>
-            <div class="product-meta">
-              <span class="product-type">{{ item.typeLabel }}</span>
-              <span v-if="item.tag" class="product-tag">{{ item.tag }}</span>
+
+            <!-- 底部信息 -->
+            <div class="product-footer">
+              <div class="buy-info">
+                <el-icon>
+                  <ShoppingCart />
+                </el-icon>
+                <span>{{ formatDate(item.buy_time) }}</span>
+              </div>
+              <div v-if="item.is_allocated == 1" class="allocated-badge">
+                <el-icon>
+                  <Share />
+                </el-icon>
+                <span>已分配</span>
+              </div>
             </div>
           </div>
         </div>
@@ -33,51 +111,141 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { onMounted, ref } from 'vue'
+import { purchasedList } from '/@/api/frontend/me'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import {
+  Link,
+  Monitor,
+  Tickets,
+  Coin,
+  Calendar,
+  ShoppingCart,
+  Share
+} from '@element-plus/icons-vue'
 
 defineOptions({ name: 'MePurchaseProduct' })
 
-type ProductType = 'web' | 'download'
-type Product = {
-  id: string
-  name: string
-  description: string
-  expireAt?: string
-  type: ProductType
-  typeLabel: string
-  link: string
-  tag?: string
+const router = useRouter()
+
+type ProductItem = {
+  product_id: number
+  product_name: string
+  product_type: 1 | 2 // 1=网页,2=软件
+  product_link: string
+  image: string
+  package_spec: string
+  package_type: 1 | 2 // 1=个人套餐,2=企业套餐
+  package_type_text: string
+  card_type: 1 | 2 | 3 | 4 | 5 | 6 | 7 // 1=次卡,2=积分卡,3=日卡,4=周卡,5=季卡,6=月卡,7=年卡
+  card_type_text: string
+  total_quantity: number
+  effective_end_time: string | null
+  remaining_times: number
+  remaining_points: number
+  display_info: string
+  usage_id: number
+  start_time: string
+  last_use_time: string | null
+  status: 1 | 2 | 3 // 1=有效,2=已过期,3=已用完
+  buy_time: string
+  is_allocated: 0 | 1 // 0=未分配,1=已分配
+  company_id: number | null
+  allocation_id: number | null
+  total_seats: number | null
+  assigned_seats: number | null
+  available_seats: number | null
+}
+
+type CompanyInfo = {
+  company_id: number
+  company_name: string
+  is_creator: boolean
+}
+
+// 产品列表
+const products = ref<ProductItem[]>([])
+// 企业信息
+const companyInfo = ref<CompanyInfo | null>(null)
+// 产品数量
+const productsNum = ref()
+
+// 获取列表
+const getList = async () => {
+  try {
+    const res = await purchasedList()
+    products.value = res.data.products || []
+    companyInfo.value = res.data.company_info || null
+    productsNum.value = res.data.total
+  } catch (error) {
+    ElMessage.error('获取产品列表失败')
+  }
+}
+
+// 跳转购买
+const gotoBuy = () => {
+  router.push('/')
+}
+
+// 获取状态文本
+const getStatusText = (status: number) => {
+  const statusMap: Record<number, string> = {
+    1: '有效',
+    2: '已过期',
+    3: '已用完'
+  }
+  return statusMap[status] || '未知'
+}
+
+// 获取状态样式类
+const getStatusClass = (status: number) => {
+  const classMap: Record<number, string> = {
+    1: 'status-active',
+    2: 'status-expired',
+    3: 'status-used-up'
+  }
+  return classMap[status] || ''
+}
+
+// 格式化日期
+const formatDate = (dateStr: string) => {
+  if (!dateStr) return ''
+  const date = new Date(dateStr)
+  const now = new Date()
+  const diff = now.getTime() - date.getTime()
+  const days = Math.floor(diff / (1000 * 60 * 60 * 24))
+
+  if (days == 0) return '今天'
+  if (days == 1) return '昨天'
+  if (days < 7) return `${days}天前`
+  if (days < 30) return `${Math.floor(days / 7)}周前`
+  if (days < 365) return `${Math.floor(days / 30)}个月前`
+
+  return dateStr.split(' ')[0]
 }
 
-const products = ref<Product[]>([
-  {
-    id: 'jude-pen',
-    name: '聚德神笔',
-    description: '聚德 AI 写作/办公套件,可在线使用多种智能工具。',
-    expireAt: '2025-12-30',
-    type: 'web',
-    typeLabel: '在线工具',
-    link: 'https://www.example-tools.com/jude-pen',
-    tag: '网页'
-  },
-  {
-    id: 'lens-tool',
-    name: '棱镜',
-    description: '图像/视频增强助手,需要下载安装客户端使用。',
-    expireAt: '2025-12-30',
-    type: 'download',
-    typeLabel: '软件下载',
-    link: 'https://www.example-tools.com/download/lens-setup.exe',
-    tag: 'Windows'
+// 点击产品卡片
+const handleProductClick = (item: ProductItem) => {
+  // 如果已过期或已用完,提示用户
+  if (item.status == 2) {
+    ElMessage.warning('该产品已过期')
+    return
+  }
+  if (item.status == 3) {
+    ElMessage.warning('该产品已用完')
+    return
   }
-])
 
-const handleProductClick = (item: Product) => {
-  if (!item?.link) return
+  if (!item.product_link) {
+    ElMessage.warning('产品链接不存在')
+    return
+  }
 
-  if (item.type === 'download') {
+  // 软件类型,触发下载
+  if (item.product_type == 2) {
     const a = document.createElement('a')
-    a.href = item.link
+    a.href = item.product_link
     a.target = '_blank'
     a.rel = 'noopener noreferrer'
     a.download = ''
@@ -87,8 +255,13 @@ const handleProductClick = (item: Product) => {
     return
   }
 
-  window.open(item.link, '_blank', 'noopener,noreferrer')
+  // 网页类型,打开新窗口
+  window.open(item.product_link, '_blank', 'noopener,noreferrer')
 }
+
+onMounted(() => {
+  getList()
+})
 </script>
 
 <style scoped lang="scss">
@@ -96,110 +269,303 @@ const handleProductClick = (item: Product) => {
   width: 100%;
 
   .product-header {
-    margin-bottom: 20px;
-    padding-bottom: 15px;
-    border-bottom: 1px solid #e4e7ed;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 24px;
+    padding-bottom: 16px;
+    border-bottom: 2px solid #e4e7ed;
 
     .product-title {
       margin: 0;
-      font-size: 20px;
+      font-size: 24px;
       font-weight: bold;
       color: #303133;
     }
+
+    .company-info {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 8px 16px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border-radius: 20px;
+      color: #fff;
+      font-size: 14px;
+
+      .el-icon {
+        font-size: 16px;
+      }
+    }
   }
 
   .product-content {
     padding: 20px 0;
-    color: #606266;
 
     .product-grid {
       display: grid;
-      grid-template-columns: repeat(auto-fill, minmax(220px, 260px));
+      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
       gap: 24px;
     }
 
     .product-card {
+      position: relative;
       cursor: pointer;
       background: #fff;
-      border-radius: 10px;
+      border-radius: 12px;
       border: 1px solid #e4e7ed;
-      transition: all 0.2s ease;
+      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
       overflow: hidden;
 
       &:hover {
         border-color: #409eff;
-        box-shadow: 0 6px 16px rgba(64, 158, 255, 0.15);
-        transform: translateY(-2px);
+        box-shadow: 0 8px 24px rgba(64, 158, 255, 0.2);
+        transform: translateY(-4px);
+      }
+
+      &.expired {
+        opacity: 0.7;
+
+        &:hover {
+          border-color: #909399;
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+        }
+      }
+    }
+
+    .status-badge {
+      position: absolute;
+      top: 12px;
+      right: 12px;
+      padding: 4px 12px;
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 600;
+      z-index: 2;
+      backdrop-filter: blur(8px);
+
+      &.status-active {
+        background: rgba(103, 194, 58, 0.9);
+        color: #fff;
+      }
+
+      &.status-expired {
+        background: rgba(245, 108, 108, 0.9);
+        color: #fff;
+      }
+
+      &.status-used-up {
+        background: rgba(144, 147, 153, 0.9);
+        color: #fff;
       }
     }
 
     .product-image-wrapper {
-      padding: 14px 14px 0;
+      position: relative;
+      padding: 16px 16px 0;
 
       img {
         display: block;
         width: 100%;
-        height: 160px;
-        border-radius: 6px;
+        height: 180px;
+        border-radius: 8px;
         object-fit: cover;
       }
+
+      .product-type-badge {
+        position: absolute;
+        bottom: 8px;
+        left: 24px;
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        padding: 6px 12px;
+        border-radius: 16px;
+        font-size: 12px;
+        font-weight: 600;
+        backdrop-filter: blur(8px);
+
+        &.web {
+          background: rgba(64, 158, 255, 0.9);
+          color: #fff;
+        }
+
+        &.software {
+          background: rgba(103, 194, 58, 0.9);
+          color: #fff;
+        }
+
+        .el-icon {
+          font-size: 14px;
+        }
+      }
     }
 
     .product-info {
-      padding: 12px 16px 16px;
+      padding: 16px;
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+    }
+
+    .product-main {
+      display: flex;
+      gap: 12px;
+      align-items: flex-end;
+    }
+
+    .product-main-left {
+      flex: 1.3;
+      min-width: 0;
       display: flex;
       flex-direction: column;
-      gap: 6px;
+      gap: 10px;
+    }
+
+    .product-main-right {
+      flex: 1;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      min-width: 150px;
     }
 
     .product-name {
       font-size: 16px;
       font-weight: 600;
       color: #303133;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .package-info {
+      display: flex;
+      gap: 8px;
+      flex-wrap: wrap;
     }
 
-    .product-desc {
+    .package-spec {
       font-size: 13px;
-      color: #909399;
+      color: #606266;
       line-height: 1.5;
-      min-height: 38px;
+      padding: 8px 12px;
+      background: #f5f7fa;
+      border-radius: 6px;
+      border-left: 3px solid #409eff;
     }
 
-    .product-expire {
-      margin-top: 2px;
-      font-size: 12px;
+    .usage-info {
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+
+      .usage-item {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        font-size: 13px;
+        color: #606266;
+        padding: 6px 10px;
+        background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%);
+        border-radius: 6px;
 
-      .expire-label {
+        .el-icon {
+          font-size: 16px;
+          color: #409eff;
+        }
+
+        strong {
+          color: #409eff;
+          font-weight: 600;
+        }
+      }
+    }
+
+    .seats-info {
+      width: 100%;
+      padding: 8px 12px;
+      background: #f5f7fa;
+      border-radius: 8px;
+      border: 1px solid #e0e6f1;
+      display: flex;
+      flex-direction: column;
+
+      .seats-row {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        font-size: 12px;
+        color: #606266;
+      }
+
+      .seats-label {
         color: #909399;
       }
 
-      .expire-text {
+      .seats-value {
+        display: flex;
+        align-items: center;
+        font-size: 12px;
         color: #606266;
       }
+
+      .seats-number {
+        font-weight: 500;
+        color: #303133;
+
+        &.strong {
+          color: #409eff;
+          font-weight: 600;
+        }
+      }
+
+      .seats-split {
+        margin: 0 4px;
+        color: #c0c4cc;
+      }
+
+      .seats-text {
+        margin-left: 6px;
+        font-size: 12px;
+        color: #909399;
+      }
     }
 
-    .product-meta {
+    .product-footer {
       display: flex;
       justify-content: space-between;
       align-items: center;
-      margin-top: 4px;
-      font-size: 12px;
-    }
+      padding-top: 12px;
+      border-top: 1px solid #e4e7ed;
 
-    .product-type {
-      padding: 3px 8px;
-      border-radius: 10px;
-      background: #ecf5ff;
-      color: #409eff;
-    }
+      .buy-info {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        font-size: 12px;
+        color: #909399;
+
+        .el-icon {
+          font-size: 14px;
+        }
+      }
 
-    .product-tag {
-      padding: 3px 8px;
-      border-radius: 10px;
-      background: #fef0f0;
-      color: #f56c6c;
+      .allocated-badge {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        padding: 4px 10px;
+        background: #e6f7ff;
+        border-radius: 12px;
+        font-size: 12px;
+        color: #1890ff;
+
+        .el-icon {
+          font-size: 14px;
+        }
+      }
     }
   }
 }
 </style>
-

+ 118 - 120
web/src/views/frontend/components/Me/subordinates.vue

@@ -2,137 +2,176 @@
   <div class="me-subordinates">
     <div class="subordinates-header">
       <h2 class="subordinates-title">我的下级</h2>
+      <p class="subordinates-subtitle">邀请人数:{{ total }}</p>
     </div>
 
     <div class="subordinates-content">
-      <!-- 过滤/搜索区域 + 汇总指标 -->
+      <!-- 过滤/搜索区域 -->
       <div class="filter-section">
         <div class="filter-left">
-          <el-date-picker
-            v-model="searchForm.dateRange"
-            class="filter-item date-picker"
-            type="daterange"
-            unlink-panels
-            start-placeholder="开始日期"
-            end-placeholder="结束日期"
-            range-separator="~"
-            value-format="yyyy-MM-dd"
-          />
-
-          <el-input
-            v-model="searchForm.name"
-            placeholder="请输入名称"
-            clearable
-            class="filter-item name-input"
-            @keyup.enter="handleSearch"
-          />
+          <el-input v-model="searchForm.username" placeholder="请输入用户名" clearable class="filter-item name-input"
+            @keyup.enter="handleSearch" />
+
+          <el-date-picker v-model="searchForm.dateRange" class="filter-item date-picker" type="daterange" unlink-panels
+            start-placeholder="开始日期" end-placeholder="结束日期" range-separator="~" value-format="YYYY-MM-DD" clearable />
 
           <el-button type="primary" :icon="Search" size="small" class="search-btn" @click="handleSearch">
             搜索
           </el-button>
-        </div>
-
-        <!-- 汇总指标 -->
-        <div class="summary-item">
-          <span class="summary-label">分拥余额:</span>
-          <span class="summary-value">
-            {{ totalCommission }}
-          </span>
+          <el-button size="small" @click="handleReset" class="search-btn">重置</el-button>
         </div>
       </div>
 
       <!-- 数据表格 -->
       <div class="table-section">
-        <el-table
-          :data="tableData"
-          style="width: 100%"
-          :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }"
-        >
+        <el-table v-if="tableData.length > 0" v-loading="loading" :data="tableData" style="width: 100%"
+          :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }">
           <el-table-column prop="id" label="ID" width="80" align="center" />
-          <el-table-column prop="userName" label="用户名称" min-width="120" align="center" />
-          <el-table-column prop="email" label="邮箱" min-width="180" align="center" />
-          <el-table-column prop="commissionAmount" label="已得分佣金额" width="140" align="center" />
-          <el-table-column prop="registerTime" label="注册时间" width="180" align="center" />
+          <el-table-column prop="username" label="用户名" min-width="120" align="center" />
+          <el-table-column prop="email" label="邮箱" min-width="200" align="center">
+            <template #default="{ row }">
+              <span>{{ row.email || '-' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="invite_time" label="邀请时间" width="180" align="center">
+            <template #default="{ row }">
+              <span>{{ row.invite_time || '-' }}</span>
+            </template>
+          </el-table-column>
           <el-table-column prop="status" label="状态" width="100" align="center">
             <template #default="{ row }">
-              <span class="status-text">{{ row.status }}</span>
+              <el-tag :type="row.status === 'enable' ? 'success' : 'danger'" size="small">
+                {{ getStatusText(row.status) }}
+              </el-tag>
             </template>
           </el-table-column>
-          <el-table-column prop="operation" label="操作" width="100" align="center">
-            <template #default>
-              <span class="status-text">详情</span>
+          <el-table-column prop="last_login_time" label="最后登录时间" width="180" align="center">
+            <template #default="{ row }">
+              <span>{{ row.last_login_time ? formatTime(row.last_login_time) : '-' }}</span>
             </template>
           </el-table-column>
         </el-table>
+        <!-- 空状态 -->
+        <el-empty v-if="!loading && tableData.length === 0" description="暂无下级用户" />
       </div>
 
       <!-- 分页控件 -->
-      <div class="pagination-section">
-        <el-pagination
-          :current-page="currentPage"
-          :page-size="pageSize"
-          :page-sizes="pageSizes"
-          :total="total"
-          layout="total, sizes, prev, pager, next, jumper"
-          background
-          @size-change="handlePageSizeChange"
-          @current-change="handleCurrentPageChange"
-        />
+      <div v-if="total > 0" class="pagination-section">
+        <el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="pageSizes" :total="total"
+          layout="total, sizes, prev, pager, next, jumper" background @size-change="handlePageSizeChange"
+          @current-change="handleCurrentPageChange" />
       </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { reactive, ref } from 'vue'
+import { onMounted, reactive, ref } from 'vue'
 import { Search } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { subordinateList } from '/@/api/frontend/me'
+import { timeFormat } from '/@/utils/common'
 
 defineOptions({ name: 'MeSubordinates' })
 
-type TableRow = {
-  id: number | string
-  userName: string
-  email: string
-  commissionAmount: number
-  registerTime: string
-  status: string
+// 接口返回的数据结构
+interface SubordinateUser {
+  id: number // id
+  username: string // 用户名
+  email: string // 电子邮箱
+  last_login_time: number // 最后登录时间(时间戳)
+  status: 'enable' | 'disable' // 状态:enable=启用,disable=禁用
+  invite_time: string // 邀请时间
 }
 
-const searchForm = reactive<{ dateRange: string[]; name: string }>({
-  dateRange: ['2025-10-01', '2025-10-01'],
-  name: ''
+const searchForm = reactive<{ dateRange?: string[]; username: string }>({
+  dateRange: undefined,
+  username: ''
 })
-const totalCommission = ref(30)
-const tableData = ref<TableRow[]>([
-  { id: '-', userName: '汇总', email: '-', commissionAmount: 30, registerTime: '-', status: '-' },
-  { id: 1, userName: '张三', email: 'XXX.qq.com', commissionAmount: 10, registerTime: '2025-10-01 14:32:11', status: '正常' },
-  { id: 10, userName: '李四', email: 'XXX.qq.com', commissionAmount: 20, registerTime: '2025-10-01 14:32:11', status: '正常' }
-])
+
+const tableData = ref<SubordinateUser[]>([])
+const loading = ref(false)
+
+// 分页数据
 const currentPage = ref(1)
 const pageSize = ref(10)
 const pageSizes = [10, 20, 30, 50]
-const total = ref(1)
+const total = ref(0)
 
-const loadData = () => {
-  // TODO: 接入实际 API 获取数据
-  console.log('搜索条件:', { ...searchForm })
+// 格式化时间戳
+const formatTime = (timestamp: number): string => {
+  if (!timestamp) return '-'
+  return timeFormat(timestamp, 'yyyy-mm-dd hh:MM:ss')
+}
+
+// 获取状态文本
+const getStatusText = (status: string): string => {
+  return status === 'enable' ? '启用' : '禁用'
+}
+
+// 获取数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    const params: Record<string, any> = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+
+    // 处理搜索条件
+    if (searchForm.username) {
+      params.username = searchForm.username
+    }
+
+    // 处理日期范围
+    if (searchForm.dateRange && searchForm.dateRange.length === 2) {
+      params.start_date = searchForm.dateRange[0]
+      params.end_date = searchForm.dateRange[1]
+    }
+
+    const res = await subordinateList(params)
+    tableData.value = res.data.list || []
+    total.value = res.data.total || 0
+  } catch (error: any) {
+    ElMessage.error(error.message || '获取下级列表失败')
+    tableData.value = []
+    total.value = 0
+  } finally {
+    loading.value = false
+  }
 }
 
+// 搜索
 const handleSearch = () => {
   currentPage.value = 1
   loadData()
 }
 
+// 重置
+const handleReset = () => {
+  searchForm.dateRange = undefined
+  searchForm.username = ''
+  currentPage.value = 1
+  loadData()
+}
+
+// 分页大小改变
 const handlePageSizeChange = (val: number) => {
   pageSize.value = val
   currentPage.value = 1
   loadData()
 }
 
+// 当前页改变
 const handleCurrentPageChange = (val: number) => {
   currentPage.value = val
   loadData()
 }
+
+// 初始化加载数据
+onMounted(() => {
+  loadData()
+})
 </script>
 
 <style scoped lang="scss">
@@ -145,11 +184,17 @@ const handleCurrentPageChange = (val: number) => {
     border-bottom: 1px solid #e4e7ed;
 
     .subordinates-title {
-      margin: 0;
+      margin: 0 0 8px 0;
       font-size: 20px;
       font-weight: 500;
       color: #303133;
     }
+
+    .subordinates-subtitle {
+      margin: 0;
+      font-size: 14px;
+      color: #909399;
+    }
   }
 
   .subordinates-content {
@@ -208,49 +253,7 @@ const handleCurrentPageChange = (val: number) => {
       }
     }
 
-    .summary-item {
-      display: flex;
-      align-items: center;
-      padding: 8px 10px;
-      border-radius: 10px;
-      box-shadow: 0 2px 6px rgba(104, 151, 238, 0.15);
-      white-space: nowrap;
-      flex-shrink: 0;
-
-      .summary-label {
-        color: #8c6e1f;
-        font-size: 14px;
-        font-weight: 500;
-      }
 
-      .summary-value {
-        position: relative;
-        color: #d48806;
-        font-weight: 600;
-        font-size: 20px;
-        letter-spacing: 0.5px;
-
-        .summary-badge {
-          position: absolute;
-          top: -10px;
-          right: -20px;
-          display: inline-flex;
-          align-items: center;
-          justify-content: center;
-          min-width: 20px;
-          height: 20px;
-          padding: 0 5px;
-          background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
-          color: #fff;
-          border-radius: 10px;
-          font-size: 11px;
-          font-weight: 600;
-          line-height: 1;
-          box-shadow: 0 2px 4px rgba(255, 152, 0, 0.4);
-          border: 2px solid #fff;
-        }
-      }
-    }
   }
 
   .table-section {
@@ -344,11 +347,6 @@ const handleCurrentPageChange = (val: number) => {
         width: 100%;
         margin-top: 8px;
       }
-
-      .summary-item {
-        width: 100%;
-        justify-content: center;
-      }
     }
 
     .pagination-section {

+ 793 - 0
web/src/views/frontend/components/Me/withdrawList.vue

@@ -0,0 +1,793 @@
+<template>
+    <div class="me-withdraw-list">
+        <div class="withdraw-header">
+            <div class="header-text">
+                <h2 class="withdraw-title">提现记录</h2>
+                <p class="withdraw-subtitle">查看您的提现申请记录和处理状态</p>
+            </div>
+            <el-button type="primary" :icon="Plus" @click="handleApplyWithdraw">申请提现</el-button>
+        </div>
+
+        <div class="stats-section">
+            <div class="stat-card stat-total">
+                <div class="stat-icon">
+                    <el-icon>
+                        <Money />
+                    </el-icon>
+                </div>
+                <div class="stat-content">
+                    <div class="stat-label">累计提现金额</div>
+                    <div class="stat-value">¥{{ formatMoney(stats.total_amount) }}</div>
+                    <div class="stat-desc">所有已完成提现的总金额</div>
+                </div>
+            </div>
+            <div class="stat-card stat-pending">
+                <div class="stat-icon">
+                    <el-icon>
+                        <Clock />
+                    </el-icon>
+                </div>
+                <div class="stat-content">
+                    <div class="stat-label">待审核金额</div>
+                    <div class="stat-value">¥{{ formatMoney(stats.pending_amount) }}</div>
+                    <div class="stat-desc">正在审核中的提现金额</div>
+                </div>
+            </div>
+            <div class="stat-card stat-success">
+                <div class="stat-icon">
+                    <el-icon>
+                        <CircleCheck />
+                    </el-icon>
+                </div>
+                <div class="stat-content">
+                    <div class="stat-label">成功提现次数</div>
+                    <div class="stat-value">{{ stats.success_count }}</div>
+                    <div class="stat-desc">已成功完成的提现次数</div>
+                </div>
+            </div>
+        </div>
+
+        <div class="withdraw-filters">
+            <el-input v-model="searchForm.withdrawNo" placeholder="请输入提现单号" clearable
+                class="filter-item withdraw-input">
+                <template #prepend>提现单号</template>
+            </el-input>
+
+            <el-date-picker v-model="searchForm.dateRange" class="filter-item date-picker" type="daterange"
+                unlink-panels start-placeholder="申请开始时间" end-placeholder="申请结束时间" range-separator="~"
+                value-format="YYYY-MM-DD" clearable />
+
+            <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
+            <el-button @click="handleReset">重置</el-button>
+        </div>
+
+        <div class="withdraw-tabs">
+            <el-tabs v-model="activeStatus" @tab-click="handleTabClick">
+                <el-tab-pane v-for="tab in statusTabs" :key="tab.value"
+                    :label="`${tab.label} (${getWithdrawCount(tab.value)})`" :name="tab.value" />
+            </el-tabs>
+        </div>
+
+        <div v-loading="loading" class="withdraw-list-wrapper">
+            <template v-if="filteredWithdraws.length">
+                <div class="withdraw-list">
+                    <div v-for="item in filteredWithdraws" :key="item.id" class="withdraw-card">
+                        <div class="withdraw-card__header">
+                            <div class="header-left">
+                                <span class="withdraw-no">提现单号:{{ item.withdraw_no }}</span>
+                                <el-tag size="small" :type="getStatusType(item.status)" class="status-tag">
+                                    {{ item.status_text }}
+                                </el-tag>
+                            </div>
+                            <div class="header-right">
+                                <span class="withdraw-time">申请时间:{{ item.create_time }}</span>
+                            </div>
+                        </div>
+
+                        <div class="withdraw-card__body">
+                            <div class="withdraw-item">
+                                <div class="item-label">提现金额</div>
+                                <div class="item-value amount">¥{{ formatMoney(item.amount) }}</div>
+                            </div>
+
+                            <div class="withdraw-item">
+                                <div class="item-label">手续费</div>
+                                <div class="item-value fee">¥{{ formatMoney(item.fee) }}</div>
+                            </div>
+
+                            <div class="withdraw-item">
+                                <div class="item-label">实际到账</div>
+                                <div class="item-value actual">¥{{ formatMoney(item.actual_amount) }}</div>
+                            </div>
+
+                            <div class="withdraw-item">
+                                <div class="item-label">提现方式</div>
+                                <div class="item-value">{{ item.withdraw_type_text }}</div>
+                            </div>
+
+                            <div class="withdraw-item">
+                                <div class="item-label">收款账户</div>
+                                <div class="item-value account">{{ item.account }}</div>
+                            </div>
+
+                            <div class="withdraw-item">
+                                <div class="item-label">处理时间</div>
+                                <div class="item-value">{{ item.process_time || '-' }}</div>
+                            </div>
+
+                            <div v-if="item.remark" class="withdraw-item full-width">
+                                <div class="item-label">备注说明</div>
+                                <div class="item-value note">{{ item.remark }}</div>
+                            </div>
+
+                            <div v-if="item.reject_reason" class="withdraw-item full-width">
+                                <div class="item-label">拒绝原因</div>
+                                <div class="item-value reject-reason">{{ item.reject_reason }}</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </template>
+            <template v-else>
+                <el-empty class="withdraw-empty" description="暂无提现记录" />
+            </template>
+        </div>
+
+        <el-dialog v-model="applyDialogVisible" title="申请提现" width="500px" :close-on-click-modal="false">
+            <el-form ref="applyFormRef" :model="applyForm" :rules="applyRules" label-width="100px">
+                <el-form-item label="可提现金额">
+                    <div class="available-amount">¥{{ formatMoney(availableAmount) }}</div>
+                </el-form-item>
+
+                <el-form-item label="提现金额" prop="amount">
+                    <el-input v-model="applyForm.amount" placeholder="请输入提现金额" type="number">
+                        <template #append>元</template>
+                    </el-input>
+                    <div class="form-tip">最低提现金额:¥100,手续费率:2%</div>
+                </el-form-item>
+
+                <el-form-item label="提现方式" prop="withdraw_type">
+                    <el-select v-model="applyForm.withdraw_type" placeholder="请选择提现方式">
+                        <el-option label="支付宝" value="alipay" />
+                        <el-option label="微信" value="wechat" />
+                        <el-option label="银行卡" value="bank" />
+                    </el-select>
+                </el-form-item>
+
+                <el-form-item label="收款账户" prop="account">
+                    <el-input v-model="applyForm.account" placeholder="请输入收款账户" />
+                </el-form-item>
+
+                <el-form-item label="账户姓名" prop="account_name">
+                    <el-input v-model="applyForm.account_name" placeholder="请输入账户姓名" />
+                </el-form-item>
+
+                <el-form-item label="备注" prop="remark">
+                    <el-input v-model="applyForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息(选填)" />
+                </el-form-item>
+            </el-form>
+
+            <template #footer>
+                <el-button @click="applyDialogVisible = false">取消</el-button>
+                <el-button type="primary" :loading="submitting" @click="handleSubmitApply">
+                    提交申请
+                </el-button>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { Search, Plus, Money, Clock, CircleCheck } from '@element-plus/icons-vue'
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
+
+defineOptions({ name: 'MeWithdrawList' })
+
+type WithdrawStatus = 'pending' | 'approved' | 'rejected' | 'processing' | 'completed'
+type WithdrawTabStatus = 'all' | WithdrawStatus
+
+type WithdrawRecord = {
+    id: number
+    withdraw_no: string
+    amount: number
+    fee: number
+    actual_amount: number
+    status: WithdrawStatus
+    status_text: string
+    withdraw_type: string
+    withdraw_type_text: string
+    account: string
+    account_name: string
+    create_time: string
+    process_time: string
+    remark: string
+    reject_reason: string
+}
+
+type WithdrawStats = {
+    total_amount: number
+    pending_amount: number
+    success_count: number
+}
+
+const activeStatus = ref<WithdrawTabStatus>('all')
+const loading = ref(false)
+const submitting = ref(false)
+const applyDialogVisible = ref(false)
+const applyFormRef = ref<FormInstance>()
+
+const searchForm = reactive<{ withdrawNo: string; dateRange: string[] }>({
+    withdrawNo: '',
+    dateRange: []
+})
+
+const appliedFilters = reactive<{ withdrawNo: string; dateRange: string[] }>({
+    withdrawNo: '',
+    dateRange: []
+})
+
+const applyForm = reactive({
+    amount: '',
+    withdraw_type: '',
+    account: '',
+    account_name: '',
+    remark: ''
+})
+
+const availableAmount = ref(0)
+
+const applyRules: FormRules = {
+    amount: [
+        { required: true, message: '请输入提现金额', trigger: 'blur' },
+        {
+            validator: (rule, value, callback) => {
+                if (value < 100) {
+                    callback(new Error('最低提现金额为100元'))
+                } else if (value > availableAmount.value) {
+                    callback(new Error('提现金额不能超过可提现金额'))
+                } else {
+                    callback()
+                }
+            },
+            trigger: 'blur'
+        }
+    ],
+    withdraw_type: [{ required: true, message: '请选择提现方式', trigger: 'change' }],
+    account: [{ required: true, message: '请输入收款账户', trigger: 'blur' }],
+    account_name: [{ required: true, message: '请输入账户姓名', trigger: 'blur' }]
+}
+
+const statusTabs: { label: string; value: WithdrawTabStatus }[] = [
+    { label: '全部', value: 'all' },
+    { label: '待审核', value: 'pending' },
+    { label: '已通过', value: 'approved' },
+    { label: '处理中', value: 'processing' },
+    { label: '已完成', value: 'completed' },
+    { label: '已拒绝', value: 'rejected' }
+]
+
+const withdraws = ref<WithdrawRecord[]>([])
+const stats = ref<WithdrawStats>({
+    total_amount: 0,
+    pending_amount: 0,
+    success_count: 0
+})
+
+const loadWithdrawRecords = async () => {
+    try {
+        loading.value = true
+        await new Promise(resolve => setTimeout(resolve, 500))
+        withdraws.value = [
+            {
+                id: 1,
+                withdraw_no: 'WD202501200001',
+                amount: 1000,
+                fee: 20,
+                actual_amount: 980,
+                status: 'completed',
+                status_text: '已完成',
+                withdraw_type: 'alipay',
+                withdraw_type_text: '支付宝',
+                account: '138****8888',
+                account_name: '张三',
+                create_time: '2025-01-15 10:30:00',
+                process_time: '2025-01-15 14:20:00',
+                remark: '正常提现',
+                reject_reason: ''
+            },
+            {
+                id: 2,
+                withdraw_no: 'WD202501200002',
+                amount: 500,
+                fee: 10,
+                actual_amount: 490,
+                status: 'pending',
+                status_text: '待审核',
+                withdraw_type: 'wechat',
+                withdraw_type_text: '微信',
+                account: 'wx_test123',
+                account_name: '李四',
+                create_time: '2025-01-18 09:15:00',
+                process_time: '',
+                remark: '',
+                reject_reason: ''
+            },
+            {
+                id: 3,
+                withdraw_no: 'WD202501200003',
+                amount: 2000,
+                fee: 40,
+                actual_amount: 1960,
+                status: 'rejected',
+                status_text: '已拒绝',
+                withdraw_type: 'bank',
+                withdraw_type_text: '银行卡',
+                account: '6222****1234',
+                account_name: '王五',
+                create_time: '2025-01-10 16:45:00',
+                process_time: '2025-01-11 10:00:00',
+                remark: '',
+                reject_reason: '账户信息不符,请核对后重新申请'
+            }
+        ]
+
+        stats.value = {
+            total_amount: 15680,
+            pending_amount: 500,
+            success_count: 12
+        }
+
+        availableAmount.value = 3580.50
+    } catch (error) {
+        console.error('加载提现记录失败:', error)
+        ElMessage.error('加载提现记录失败,请稍后重试')
+    } finally {
+        loading.value = false
+    }
+}
+
+const baseFilteredWithdraws = computed(() => {
+    let filtered = withdraws.value
+
+    if (appliedFilters.withdrawNo) {
+        const withdrawNo = appliedFilters.withdrawNo.trim()
+        filtered = filtered.filter(w => w.withdraw_no.includes(withdrawNo))
+    }
+
+    if (appliedFilters.dateRange && appliedFilters.dateRange.length === 2) {
+        filtered = filtered.filter(w => {
+            return isWithinRange(w.create_time, appliedFilters.dateRange)
+        })
+    }
+
+    return filtered
+})
+
+const filteredWithdraws = computed(() => {
+    let filtered = baseFilteredWithdraws.value
+
+    if (activeStatus.value !== 'all') {
+        filtered = filtered.filter(w => w.status === activeStatus.value)
+    }
+
+    return filtered
+})
+
+const filteredStats = computed(() => {
+    const base = baseFilteredWithdraws.value
+    return {
+        all: base.length,
+        pending: base.filter(w => w.status === 'pending').length,
+        approved: base.filter(w => w.status === 'approved').length,
+        processing: base.filter(w => w.status === 'processing').length,
+        completed: base.filter(w => w.status === 'completed').length,
+        rejected: base.filter(w => w.status === 'rejected').length
+    }
+})
+
+const formatMoney = (amount: number | undefined) => {
+    if (amount === undefined || amount === null) return '0.00'
+    return Number(amount).toFixed(2)
+}
+
+const getStatusType = (status: WithdrawStatus) => {
+    const typeMap: Record<WithdrawStatus, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
+        pending: 'warning',
+        approved: 'success',
+        processing: 'primary',
+        completed: 'success',
+        rejected: 'danger'
+    }
+    return typeMap[status] || 'info'
+}
+
+const handleSearch = () => {
+    appliedFilters.withdrawNo = searchForm.withdrawNo || ''
+    if (searchForm.dateRange && Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
+        appliedFilters.dateRange = [...searchForm.dateRange]
+    } else {
+        appliedFilters.dateRange = []
+    }
+}
+
+const handleReset = () => {
+    searchForm.withdrawNo = ''
+    searchForm.dateRange = []
+    appliedFilters.withdrawNo = ''
+    appliedFilters.dateRange = []
+}
+
+const handleTabClick = (tab: { paneName?: string | number; name?: string | number }) => {
+    activeStatus.value = (tab.paneName || tab.name || 'all') as WithdrawTabStatus
+}
+
+const getWithdrawCount = (status: WithdrawTabStatus) => {
+    if (status === 'all') return filteredStats.value.all || 0
+    return filteredStats.value[status] || 0
+}
+
+const isWithinRange = (time: string, range: string[]) => {
+    if (!range || range.length !== 2 || !range[0] || !range[1]) return true
+    const [start, end] = range
+    const startDate = new Date(start + ' 00:00:00')
+    const endDate = new Date(end + ' 23:59:59')
+    const targetDate = new Date(time.replace(/-/g, '/'))
+    if (isNaN(startDate.getTime()) || isNaN(endDate.getTime()) || isNaN(targetDate.getTime())) {
+        return true
+    }
+    return targetDate >= startDate && targetDate <= endDate
+}
+
+const handleApplyWithdraw = () => {
+    applyDialogVisible.value = true
+    applyForm.amount = ''
+    applyForm.withdraw_type = ''
+    applyForm.account = ''
+    applyForm.account_name = ''
+    applyForm.remark = ''
+}
+
+const handleSubmitApply = async () => {
+    if (!applyFormRef.value) return
+
+    await applyFormRef.value.validate((valid) => {
+        if (valid) {
+            submitting.value = true
+            setTimeout(() => {
+                submitting.value = false
+                applyDialogVisible.value = false
+                ElMessage.success('提现申请已提交,请等待审核')
+                loadWithdrawRecords()
+            }, 1000)
+        }
+    })
+}
+
+onMounted(() => {
+    loadWithdrawRecords()
+})
+</script>
+
+<style scoped lang="scss">
+.me-withdraw-list {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+}
+
+.withdraw-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-wrap: wrap;
+    gap: 12px;
+
+    .header-text {
+        .withdraw-title {
+            margin: 0;
+            font-size: 24px;
+            font-weight: 600;
+            color: #1f2f3d;
+        }
+
+        .withdraw-subtitle {
+            margin: 6px 0 0;
+            font-size: 14px;
+            color: #909399;
+        }
+    }
+}
+
+.stats-section {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 20px;
+
+    @media (max-width: 1024px) {
+        grid-template-columns: repeat(2, 1fr);
+    }
+
+    @media (max-width: 768px) {
+        grid-template-columns: 1fr;
+    }
+
+    .stat-card {
+        display: flex;
+        align-items: flex-start;
+        padding: 20px;
+        background: #ffffff;
+        border-radius: 12px;
+        box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
+        transition: all 0.3s ease;
+        border: 1px solid #d9ecff;
+
+        &:hover {
+            box-shadow: 0 6px 20px rgba(64, 158, 255, 0.18);
+            transform: translateY(-3px);
+        }
+
+        .stat-icon {
+            width: 56px;
+            height: 56px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 12px;
+            margin-right: 16px;
+            flex-shrink: 0;
+            font-size: 28px;
+            color: #fff;
+        }
+
+        &.stat-total .stat-icon {
+            background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+        }
+
+        &.stat-pending .stat-icon {
+            background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
+        }
+
+        &.stat-success .stat-icon {
+            background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
+        }
+
+        .stat-content {
+            flex: 1;
+
+            .stat-label {
+                font-size: 14px;
+                color: #909399;
+                margin-bottom: 8px;
+                font-weight: 500;
+            }
+
+            .stat-value {
+                font-size: 26px;
+                font-weight: 700;
+                color: #303133;
+                margin-bottom: 6px;
+            }
+
+            .stat-desc {
+                font-size: 12px;
+                color: #C0C4CC;
+                line-height: 1.4;
+            }
+        }
+    }
+}
+
+.withdraw-filters {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 12px;
+    padding: 16px;
+    background: linear-gradient(135deg, #ecf5ff 0%, #f5f9ff 100%);
+    border-radius: 8px;
+    border: 1px solid #c6e2ff;
+
+    .filter-item {
+        flex: 1;
+        min-width: 200px;
+    }
+
+    .withdraw-input {
+        max-width: 320px;
+    }
+
+    .date-picker {
+        max-width: 360px;
+    }
+}
+
+.withdraw-tabs {
+    background: #fff;
+    border-radius: 8px;
+    padding: 0 16px;
+    box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
+
+    ::v-deep .el-tabs__item {
+        font-weight: 500;
+    }
+
+    ::v-deep .el-tabs__active-bar {
+        height: 3px;
+        background: linear-gradient(90deg, #409EFF, #66b1ff);
+    }
+
+    ::v-deep .el-tabs__item.is-active {
+        color: #409EFF;
+    }
+}
+
+.withdraw-list-wrapper {
+    min-height: 200px;
+}
+
+.withdraw-list {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+}
+
+.withdraw-card {
+    background: #fff;
+    border-radius: 12px;
+    border: 1px solid #d9ecff;
+    box-shadow: 0 6px 18px rgba(64, 158, 255, 0.12);
+    overflow: hidden;
+    transition: all 0.3s ease;
+
+    &:hover {
+        box-shadow: 0 8px 24px rgba(64, 158, 255, 0.18);
+        transform: translateY(-2px);
+    }
+
+    &__header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        flex-wrap: wrap;
+        gap: 10px;
+        padding: 16px 20px;
+        background: linear-gradient(135deg, #ecf5ff, #ffffff);
+    }
+
+    .header-left {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+    }
+
+    .withdraw-no {
+        font-weight: 600;
+        color: #1f2d3d;
+        font-size: 15px;
+    }
+
+    .status-tag {
+        font-weight: 500;
+    }
+
+    .header-right {
+        color: #606266;
+        font-size: 13px;
+    }
+
+    .withdraw-time {
+        white-space: nowrap;
+    }
+
+    .withdraw-card__body {
+        padding: 20px;
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(180px, max-content));
+        gap: 16px 24px;
+        justify-content: flex-start;
+    }
+}
+
+.withdraw-item {
+    display: inline-flex;
+    flex-direction: column;
+    gap: 6px;
+    font-size: 14px;
+    width: max-content;
+    min-width: 0;
+
+    .item-label {
+        color: #909399;
+    }
+
+    .item-value {
+        color: #303133;
+        font-weight: 500;
+
+        &.amount {
+            font-size: 20px;
+            color: #409EFF;
+            font-weight: 700;
+        }
+
+        &.fee {
+            color: #E6A23C;
+            font-weight: 600;
+        }
+
+        &.actual {
+            font-size: 18px;
+            color: #67C23A;
+            font-weight: 700;
+        }
+
+        &.account {
+            font-family: 'Courier New', Courier, monospace;
+        }
+
+        &.note {
+            padding: 10px;
+            background: #f0f5ff;
+            border-radius: 6px;
+            color: #606266;
+            line-height: 1.6;
+        }
+
+        &.reject-reason {
+            padding: 10px;
+            background: #fef0f0;
+            border-radius: 6px;
+            color: #F56C6C;
+            line-height: 1.6;
+        }
+    }
+
+    &.full-width {
+        grid-column: 1 / -1;
+        width: 100%;
+    }
+}
+
+.withdraw-empty {
+    padding: 60px 0;
+    background: #fff;
+    border-radius: 12px;
+    border: 1px dashed #c6e2ff;
+}
+
+.available-amount {
+    font-size: 24px;
+    font-weight: 700;
+    color: #409EFF;
+}
+
+.form-tip {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+}
+
+@media (max-width: 768px) {
+    .withdraw-header {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+
+    .withdraw-filters {
+        flex-direction: column;
+
+        .filter-item,
+        .withdraw-input,
+        .date-picker {
+            width: 100%;
+            max-width: none;
+        }
+    }
+
+    .withdraw-card__body {
+        grid-template-columns: 1fr;
+    }
+}
+</style>

+ 398 - 0
web/src/views/frontend/components/PayQrCodeDialog.vue

@@ -0,0 +1,398 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    title="扫码支付"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :destroy-on-close="true"
+    class="pay-qrcode-dialog"
+    width="500px"
+    @close="handleClose"
+  >
+    <div class="pay-content">
+      <!-- 订单信息 -->
+      <div class="order-info">
+        <div class="info-item">
+          <span class="label">订单号:</span>
+          <span class="value">{{ orderInfo.order_no || orderInfo.sn || '-' }}</span>
+        </div>
+        <div class="info-item">
+          <span class="label">商品名称:</span>
+          <span class="value">{{ orderInfo.package_name || orderInfo.title || orderInfo.product_name || '-' }}</span>
+        </div>
+        <div class="info-item">
+          <span class="label">购买数量:</span>
+          <span class="value">{{ orderInfo.quantity || '1' }}</span>
+        </div>
+        <div class="info-item">
+          <span class="label">订单金额:</span>
+          <span class="value amount">¥{{ formatAmount(orderInfo.total_amount || orderInfo.amount || orderInfo.total_price || 0) }}</span>
+        </div>
+      </div>
+
+      <!-- 二维码区域 -->
+      <div class="qr-code-box">
+        <div v-if="loading" class="loading-box">
+          <el-icon class="is-loading"><Loading /></el-icon>
+          <p>正在生成二维码...</p>
+        </div>
+        <div v-else-if="qrCodeUrl" class="qr-code-wrapper">
+          <QrcodeVue :value="qrCodeUrl" :size="220" :margin="0" level="H" />
+          <div v-if="payStatus === 'success'" class="pay-success-overlay">
+            <el-icon class="success-icon"><CircleCheck /></el-icon>
+            <p>支付成功</p>
+          </div>
+        </div>
+        <div v-else class="error-box">
+          <el-icon class="error-icon"><CircleClose /></el-icon>
+          <p>二维码生成失败</p>
+        </div>
+      </div>
+
+      <!-- 支付提示 -->
+      <el-alert
+        v-if="qrCodeUrl && payStatus !== 'success'"
+        :closable="false"
+        type="success"
+        class="pay-tips"
+      >
+        <template #default>
+          <div class="tips-content">
+            <el-icon class="tips-icon">
+              <ChatDotRound v-if="payMethod === 'wechat'" />
+              <Coin v-else />
+            </el-icon>
+            <span v-if="payMethod === 'wechat'">请使用微信扫码支付</span>
+            <span v-else>请使用支付宝扫码支付</span>
+            <span v-if="countdown > 0" class="countdown-text">(剩余 {{ formatTime(countdown) }})</span>
+          </div>
+        </template>
+      </el-alert>
+
+      <!-- 支付成功提示 -->
+      <el-alert
+        v-if="payStatus === 'success'"
+        :closable="false"
+        type="success"
+        class="pay-success-tips"
+      >
+        <template #default>
+          <div class="tips-content">
+            <el-icon class="tips-icon"><CircleCheck /></el-icon>
+            <span>支付成功,页面将自动跳转</span>
+          </div>
+        </template>
+      </el-alert>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleClose">取消支付</el-button>
+        <el-button v-if="payStatus === 'success'" type="primary" @click="handleConfirm">
+          确定
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref, watch, onBeforeUnmount } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Loading, CircleCheck, CircleClose, ChatDotRound, Coin } from '@element-plus/icons-vue'
+import QrcodeVue from 'qrcode.vue'
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  orderInfo: {
+    type: Object,
+    default: () => ({})
+  },
+  qrCodeUrl: {
+    type: String,
+    default: ''
+  },
+  payMethod: {
+    type: String,
+    default: 'wechat'
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  },
+  payStatus: {
+    type: String,
+    default: '' // '', 'success', 'failed'
+  }
+})
+
+const emit = defineEmits(['update:visible', 'close', 'confirm', 'expired'])
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+})
+
+const handleClose = () => {
+  emit('close')
+  dialogVisible.value = false
+}
+
+const handleConfirm = () => {
+  emit('confirm')
+  dialogVisible.value = false
+}
+
+// 格式化金额显示
+const formatAmount = (amount) => {
+  if (amount === null || amount === undefined || amount === '') {
+    return '0.00'
+  }
+  const num = Number(amount)
+  if (isNaN(num)) {
+    return '0.00'
+  }
+  return num.toFixed(2)
+}
+
+// 倒计时相关
+const countdown = ref(0)
+const countdownTimer = ref(null)
+
+// 格式化时间显示(MM:SS)
+const formatTime = (seconds) => {
+  const mins = Math.floor(seconds / 60)
+  const secs = seconds % 60
+  return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
+}
+
+// 开始倒计时
+const startCountdown = () => {
+  // 清除之前的定时器
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+  }
+  
+  // 从订单信息中获取过期时间(秒)
+  const expireTime = props.orderInfo?.expire_time
+  if (expireTime && expireTime > 0) {
+    countdown.value = expireTime
+    
+    countdownTimer.value = setInterval(() => {
+      if (countdown.value > 0) {
+        countdown.value--
+      } else {
+        // 倒计时结束
+        clearInterval(countdownTimer.value)
+        countdownTimer.value = null
+        ElMessage.warning('支付二维码已过期,请重新下单')
+        emit('expired')
+      }
+    }, 1000)
+  }
+}
+
+// 停止倒计时
+const stopCountdown = () => {
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+    countdownTimer.value = null
+  }
+  countdown.value = 0
+}
+
+// 监听弹窗显示状态和订单信息变化
+watch(() => props.visible, (newVal) => {
+  if (newVal && props.orderInfo?.expire_time) {
+    startCountdown()
+  } else {
+    stopCountdown()
+  }
+})
+
+watch(() => props.orderInfo?.expire_time, (newVal) => {
+  if (props.visible && newVal) {
+    startCountdown()
+  }
+})
+
+// 监听支付状态,支付成功时停止倒计时
+watch(() => props.payStatus, (newVal) => {
+  if (newVal === 'success') {
+    stopCountdown()
+  }
+})
+
+// 组件卸载时清除定时器
+onBeforeUnmount(() => {
+  stopCountdown()
+})
+</script>
+
+<style scoped lang="scss">
+.pay-qrcode-dialog {
+  :deep(.el-dialog__body) {
+    padding: 20px;
+  }
+}
+
+.pay-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 20px;
+}
+
+.order-info {
+  width: 100%;
+  padding: 16px;
+  background: #f5f7fa;
+  border-radius: 8px;
+  
+  .info-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    .label {
+      font-size: 14px;
+      color: #606266;
+    }
+    
+    .value {
+      font-size: 14px;
+      color: #303133;
+      font-weight: 500;
+      
+      &.amount {
+        color: #f56c6c;
+        font-size: 18px;
+        font-weight: 600;
+      }
+    }
+  }
+}
+
+.qr-code-box {
+  position: relative;
+  width: 240px;
+  height: 240px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  
+  .loading-box {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+    color: #909399;
+    
+    .el-icon {
+      font-size: 32px;
+    }
+    
+    p {
+      margin: 0;
+      font-size: 14px;
+    }
+  }
+  
+  .qr-code-wrapper {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  
+  .pay-success-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.95);
+    border-radius: 8px;
+    
+    .success-icon {
+      font-size: 48px;
+      color: #67c23a;
+      margin-bottom: 8px;
+    }
+    
+    p {
+      margin: 0;
+      font-size: 16px;
+      color: #67c23a;
+      font-weight: 500;
+    }
+  }
+  
+  .error-box {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+    color: #f56c6c;
+    
+    .error-icon {
+      font-size: 32px;
+    }
+    
+    p {
+      margin: 0;
+      font-size: 14px;
+    }
+  }
+}
+
+.pay-tips,
+.pay-success-tips {
+  width: 100%;
+  
+  .tips-content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    
+    .tips-icon {
+      font-size: 16px;
+    }
+    
+    span {
+      font-size: 14px;
+    }
+    
+    .countdown-text {
+      color: #f56c6c;
+      font-weight: 500;
+      margin-left: 4px;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>
+

Fișier diff suprimat deoarece este prea mare
+ 496 - 282
web/src/views/frontend/components/ProductSeatDialog.vue


+ 94 - 10
web/src/views/frontend/components/goods.vue

@@ -9,11 +9,15 @@
         >
           <!-- 商品图片 -->
           <div class="goods-image-wrapper">
-            <img :src="item.img" :alt="item.name" class="goods-image">
+            <img :src="item.image" :alt="item.name" class="goods-image">
             <!-- 自动发货标签 -->
             <div class="auto-delivery-badge" :class="item.type === 1 ? 'badge-personal' : 'badge-enterprise'">
               <span>{{ item.type === 1 ? '个人套餐' : '企业套餐' }}</span>
             </div>
+            <!-- 卡类型标签(新结构:card_info 优先,兼容 card_type 映射) -->
+            <div class="card-type-badge" :class="getCardTypeBadgeClass(item)">
+              <span>{{ getCardTypeText(item) }}</span>
+            </div>
           </div>
   
           <!-- 商品信息 -->
@@ -29,13 +33,13 @@
   
             <!-- 库存和已售信息 -->
             <div class="goods-meta">
-              <div class="inventory-status" :class="getInventoryClass(item.status)">
+              <div class="inventory-status" :class="getInventoryClass(item.stock_text)">
                 <span class="meta-label">库存:</span>
-                <span class="meta-value">{{ getInventoryText(item.status) }}</span>
+                <span class="meta-value">{{ getInventoryText(item.stock_text) }}</span>
               </div>
               <div class="sold-count">
                 <span class="meta-label">已售:</span>
-                <span class="meta-value">{{ item.sold }}</span>
+                <span class="meta-value">{{ item.sold_count }}</span>
               </div>
             </div>
           </div>
@@ -45,16 +49,23 @@
   </template>
   
 <script setup lang="ts">
-type GoodsStatus = '充足' | '一般' | '即将售罄' | '缺货' | string
+type GoodsStatus = '充足' | '一般' | '即将售罄' | '售罄' | string
 
 type GoodsItem = {
   id: string | number
   name: string
-  img: string
+  image: string
   price: number
-  sold: number
-  status: GoodsStatus
+  price_text: string
+  sold_count: number
+  stock_text: GoodsStatus
   type: number
+  // 新卡类型结构:优先使用 card_info(接口可能仍返回 card_type 作为兼容字段)
+  card_type?: number
+  card_info?: {
+    name?: string
+    show_value?: string
+  }
 }
 
 const props = withDefaults(
@@ -75,7 +86,7 @@ const getInventoryText = (status: GoodsStatus) => {
     充足: '充足',
     一般: '一般',
     即将售罄: '即将售罄',
-    缺货: '缺货'
+    售罄: '售罄'
   }
   return statusMap[status] || status
 }
@@ -84,7 +95,7 @@ const getInventoryClass = (status: GoodsStatus) => {
   if (status === '即将售罄') {
     return 'status-warning'
   }
-  if (status === '缺货') {
+  if (status === '售罄') {
     return 'status-danger'
   }
   if (status === '一般') {
@@ -93,6 +104,37 @@ const getInventoryClass = (status: GoodsStatus) => {
   return 'status-adequate'
 }
 
+const getCardTypeText = (item: GoodsItem) => {
+  // 新结构优先
+  if (item?.card_info?.name) {
+    const cardName = item.card_info.name
+    const showValue = item.card_info.show_value
+    return showValue ? `${cardName}(${showValue})` : cardName
+  }
+  // 兼容老结构
+  const map: Record<number, string> = {
+    1: '次卡',
+    2: '按积分',
+    3: '日卡',
+    4: '周卡',
+    5: '季卡',
+    6: '月卡',
+    7: '年卡'
+  }
+  const label = map[Number(item?.card_type)] || ''
+  return label || '未知类型'
+}
+
+const getCardTypeBadgeClass = (item: GoodsItem) => {
+  const t = Number(item?.card_type)
+  // 1/2(次卡/积分)一组色,3+(日期卡)一组色;未知则中性
+  if ([1, 2].includes(t)) return 'badge-usage'
+  if ([3, 4, 5, 6, 7].includes(t)) return 'badge-date'
+  // 如果只有 card_info 但没 card_type,也给一个日期卡风格(多数是日/周/月/年)
+  if (item?.card_info?.name) return 'badge-date'
+  return 'badge-unknown'
+}
+
 const handleCardClick = (item: GoodsItem) => {
   emit('handleClick', item)
 }
@@ -207,6 +249,48 @@ const handleCardClick = (item: GoodsItem) => {
           }
         }
       }
+
+      .card-type-badge {
+        position: absolute;
+        top: 12px;
+        left: 90px;
+        border-radius: 16px;
+        padding: 4px 12px;
+        transition: all 0.3s ease;
+
+        span {
+          color: #ffffff;
+          font-size: 12px;
+          font-weight: 500;
+          letter-spacing: 0.3px;
+        }
+
+        &.badge-usage {
+          background: linear-gradient(135deg, #67c23a 0%, #95d475 100%);
+          box-shadow: 0 2px 4px rgba(103, 194, 58, 0.3);
+        }
+
+        &.badge-date {
+          background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%);
+          box-shadow: 0 2px 4px rgba(144, 147, 153, 0.3);
+        }
+
+        &.badge-unknown {
+          background: rgba(0, 0, 0, 0.45);
+          backdrop-filter: blur(2px);
+        }
+
+        @media (max-width: 768px) {
+          top: 8px;
+          right: 8px;
+          padding: 3px 10px;
+          border-radius: 12px;
+
+          span {
+            font-size: 11px;
+          }
+        }
+      }
     }
   
     .goods-info {

+ 12 - 6
web/src/views/frontend/components/header.vue

@@ -26,7 +26,7 @@
   
         <!-- 中间:PC 显示搜索框,移动端显示“购物”入口 -->
         <div class="header-center">
-          <div v-if="!isMobile" class="search-box">
+          <!-- <div v-if="!isMobile" class="search-box">
             <el-icon class="search-icon">
               <Search />
             </el-icon>
@@ -37,9 +37,9 @@
               class="search-input"
               @keyup.enter="handleSearch"
             >
-          </div>
+          </div> -->
           <div
-            v-else
+            v-if="isMobile"
             class="mobile-shop-entry"
             @click="goToShopping"
           >
@@ -235,8 +235,11 @@ const goToShopping = () => {
 }
 
 const goToOrders = () => {
-  // 跳转到订单查询页面
-  router.push('/user/me')
+  // 跳转到订单查询页面,并激活购买记录tab
+  router.push({
+    path: '/user/me',
+    query: { tab: 'purchase-history' }
+  })
   closeAllMenus()
 }
 
@@ -247,7 +250,10 @@ const goToLogin = () => {
 
 const goToProfile = () => {
   // 跳转到个人中心
-  router.push('/user/me')
+  router.push({
+    path: '/user/me',
+    query: { tab: 'home' }
+  })
   closeAllMenus()
 }
 

+ 162 - 38
web/src/views/frontend/components/success.vue

@@ -7,18 +7,18 @@
     <div class="success-body">
       <el-card class="success-card" shadow="hover">
         <!-- 顶部成功图标与标题 -->
-        <div class="success-header">
+        <div class="success-header" :class="`status-${statusLevel}`">
           <div class="success-icon-wrapper">
             <div class="success-icon">
               <el-icon class="success-icon-inner">
-                <SuccessFilled />
+                <component :is="statusIcon" />
               </el-icon>
             </div>
           </div>
           <div class="success-text">
-            <h2 class="success-title">下单成功</h2>
+            <h2 class="success-title">{{ statusTitle }}</h2>
             <p class="success-subtitle">
-              您的订单已经创建成功,请尽快完成支付或前往订单中心查看详情。
+              {{ statusSubtitle }}
             </p>
           </div>
         </div>
@@ -38,6 +38,20 @@
             <div class="info-value">{{ orderInfo.orderId }}</div>
           </div>
 
+          <div class="info-row">
+            <div class="info-label">订单状态</div>
+            <div class="info-value">
+              <el-tag
+                v-if="orderInfo.payStatusText || orderInfo.orderStatusText"
+                :type="statusTagType"
+                size="small"
+              >
+                {{ orderInfo.payStatusText || orderInfo.orderStatusText }}
+              </el-tag>
+              <span v-else>--</span>
+            </div>
+          </div>
+
           <div class="info-row">
             <div class="info-label">商品名称</div>
             <div class="info-value">{{ orderInfo.goodsName }}</div>
@@ -52,14 +66,6 @@
             <div class="info-label">下单时间</div>
             <div class="info-value">{{ orderInfo.createdAt }}</div>
           </div>
-
-          <div
-            v-if="orderInfo.contact"
-            class="info-row"
-          >
-            <div class="info-label">接收邮箱</div>
-            <div class="info-value">{{ orderInfo.contact }}</div>
-          </div>
         </div>
 
         <!-- 操作按钮 -->
@@ -96,10 +102,18 @@
 </template>
 
 <script setup>
-import { computed, onMounted, reactive } from 'vue'
+import { computed, onMounted, reactive, ref } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
-import { SuccessFilled } from '@element-plus/icons-vue'
+import {
+  SuccessFilled,
+  WarningFilled,
+  CircleCloseFilled,
+  InfoFilled
+} from '@element-plus/icons-vue'
 import Header from './header.vue'
+import { checkOrderStatus } from '/@/api/frontend/order'
+
+
 
 defineOptions({ name: 'ClientOrderSuccess' })
 
@@ -108,11 +122,15 @@ const router = useRouter()
 
 const orderInfo = reactive({
   orderId: '',
+  orderNo: '',
   goodsName: '',
   amount: 0,
   payMethod: '',
   contact: '',
-  createdAt: ''
+  createdAt: '',
+  payStatus: '', // 1已支付 0待支付 2失败 3取消
+  payStatusText: '',
+  orderStatusText: ''
 })
 
 const displayAmount = computed(() => {
@@ -129,32 +147,108 @@ const displayPayMethod = computed(() => {
   return map[orderInfo.payMethod] || orderInfo.payMethod
 })
 
-const initFromRoute = () => {
+// 订单状态级别:success / warning / danger / info
+const statusLevel = computed(() => {
+  const status = String(orderInfo.payStatus || '')
+  if (status === '1') return 'success'
+  if (status === '0' || status === '') return 'pending'
+  if (status === '2' || status === '3') return 'failed'
+  return 'info'
+})
+
+// 不同状态使用不同图标
+const statusIcon = computed(() => {
+  if (statusLevel.value === 'success') return SuccessFilled
+  if (statusLevel.value === 'failed') return CircleCloseFilled
+  if (statusLevel.value === 'pending') return WarningFilled
+  return InfoFilled
+})
+
+const statusTitle = computed(() => {
+  if (statusLevel.value === 'success') return '支付成功'
+  if (statusLevel.value === 'failed') return '订单未完成'
+  if (statusLevel.value === 'pending') return '订单待支付'
+  return '订单状态'
+})
+
+const statusSubtitle = computed(() => {
+  if (statusLevel.value === 'success') {
+    return orderInfo.orderStatusText || '您的订单已支付成功,可前往订单中心查看详情。'
+  }
+  if (statusLevel.value === 'failed') {
+    return orderInfo.payStatusText || '订单已取消或支付失败,如有疑问可联系商家处理。'
+  }
+  if (statusLevel.value === 'pending') {
+    return '订单已创建但尚未完成支付,您可以前往订单中心继续支付或查看详情。'
+  }
+  return '您可以在订单中心查看当前订单的详细状态。'
+})
+
+const statusTagType = computed(() => {
+  if (statusLevel.value === 'success') return 'success'
+  if (statusLevel.value === 'failed') return 'danger'
+  if (statusLevel.value === 'pending') return 'warning'
+  return 'info'
+})
+
+const loading = ref(false)
+
+// 从后台查询订单详情
+const fetchOrderDetail = async () => {
   const query = route?.query || {}
-  const now = new Date()
-  const defaultDate = `${now.getFullYear()}-${(now.getMonth() + 1)
-    .toString()
-    .padStart(2, '0')}-${now
-    .getDate()
-    .toString()
-    .padStart(2, '0')} ${now
-    .getHours()
-    .toString()
-    .padStart(2, '0')}:${now
-    .getMinutes()
-    .toString()
-    .padStart(2, '0')}`
-
-  orderInfo.orderId = query.orderId || query.out_trade_no || '示例订单号 202312030001'
-  orderInfo.goodsName = query.goodsName || query.subject || '惊鸿·日卡'
-  orderInfo.amount = query.amount || query.totalAmount || query.total_fee || 25
-  orderInfo.payMethod = query.payMethod || query.pay_method || 'alipay'
-  orderInfo.contact = query.email || query.contact || ''
-  orderInfo.createdAt = query.createdAt || defaultDate
+  const orderId = query.order_id
+  const orderNo = query.order_no
+
+  // 如果没有携带必要参数,直接返回(可以根据需要做兜底处理)
+  if (!orderId && !orderNo) {
+    return
+  }
+
+  loading.value = true
+  try {
+    const params = {}
+    if (orderId) params.order_id = orderId
+    if (orderNo) params.order_no = orderNo
+
+    const res = await checkOrderStatus(params)
+    const order = res?.data?.order || res?.data
+    if (!order) return
+
+    // 将接口返回的数据映射到页面展示所需字段
+    orderInfo.orderId = order.order_no || order.id || ''
+    orderInfo.orderNo = order.order_no || ''
+    orderInfo.goodsName =
+      order.remark ||
+      order.package_name ||
+      order.product_name ||
+      order.title ||
+      ''
+    orderInfo.amount = order.pay_amount || order.total_amount || 0
+    orderInfo.payMethod = order.pay_method || ''
+    orderInfo.createdAt =
+      order.order_time_formatted ||
+      order.order_time ||
+      order.create_time_formatted ||
+      order.create_time ||
+      ''
+
+    // 状态相关
+    const rawPayStatus = order.pay_status
+    orderInfo.payStatus = rawPayStatus != null ? String(rawPayStatus) : ''
+    orderInfo.payStatusText = order.pay_status_text || ''
+    orderInfo.orderStatusText = order.order_status_text || ''
+  } catch (e) {
+    console.error('加载订单详情失败', e)
+  } finally {
+    loading.value = false
+  }
 }
 
 const goToOrders = () => {
-  router.push('/user/orders')
+  router.push({
+    path: '/user/me',
+    query: { tab: 'purchase-history' }
+  })
 }
 
 const goHome = () => {
@@ -162,7 +256,7 @@ const goHome = () => {
 }
 
 onMounted(() => {
-  initFromRoute()
+  fetchOrderDetail()
 })
 </script>
 
@@ -236,6 +330,36 @@ onMounted(() => {
   }
 }
 
+.success-header.status-success .success-icon {
+  background: #f0f9eb;
+  color: #67c23a;
+  box-shadow: 0 0 0 6px rgba(103, 194, 58, 0.12);
+}
+
+.success-header.status-pending .success-icon {
+  background: #fdf6ec;
+  color: #e6a23c;
+  box-shadow: 0 0 0 6px rgba(230, 162, 60, 0.16);
+}
+
+.success-header.status-failed .success-icon {
+  background: #fef0f0;
+  color: #f56c6c;
+  box-shadow: 0 0 0 6px rgba(245, 108, 108, 0.16);
+}
+
+.success-header.status-success .success-title {
+  color: #303133;
+}
+
+.success-header.status-pending .success-title {
+  color: #e6a23c;
+}
+
+.success-header.status-failed .success-title {
+  color: #f56c6c;
+}
+
 .success-text {
   flex: 1;
   min-width: 0;

+ 251 - 308
web/src/views/frontend/index.vue

@@ -1,347 +1,290 @@
 <template>
-    <div class="client-home">
-      <!-- 头部 -->
-      <Header />
-      <!-- 购买商品层 -->
-      <div class="home-goods">
-        <el-card class="box-card">
-          <template #header>
-            <div class="clearfix">
-              <span>
-                <el-icon>
-                  <Goods />
-                </el-icon>
-                商品列表
-              </span>
-            </div>
-          </template>
-          <div class="goods-list">
-            <!-- 商品列表 -->
-            <div class="goods-list-option">
-              <div class="option-scroll-wrapper">
-                <div
-                  v-for="(category, index) in goodsCategories"
-                  :key="index"
-                  class="option-item"
-                  :class="{ active: selectedCategory === index }"
-                  @click="selectCategory(index)"
-                >
-                  <div class="option-icon">
-                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-                      <path
-                        d="M12 2L2 7L12 12L22 7L12 2Z"
-                        stroke="currentColor"
-                        stroke-width="2"
-                        stroke-linecap="round"
-                        stroke-linejoin="round"
-                      />
-                      <path
-                        d="M2 17L12 22L22 17"
-                        stroke="currentColor"
-                        stroke-width="2"
-                        stroke-linecap="round"
-                        stroke-linejoin="round"
-                      />
-                      <path
-                        d="M2 12L12 17L22 12"
-                        stroke="currentColor"
-                        stroke-width="2"
-                        stroke-linecap="round"
-                        stroke-linejoin="round"
-                      />
-                    </svg>
-                  </div>
-                  <span class="option-text">{{ category.name }}</span>
+  <div class="client-home">
+    <!-- 头部 -->
+    <Header />
+    <!-- 购买商品层 -->
+    <div class="home-goods">
+      <el-card class="box-card">
+        <template #header>
+          <div class="clearfix">
+            <span>
+              <el-icon>
+                <Goods />
+              </el-icon>
+              商品列表
+            </span>
+          </div>
+        </template>
+        <div class="goods-list">
+          <!-- 商品列表 -->
+          <div class="goods-list-option">
+            <div class="option-scroll-wrapper">
+              <div v-for="(category, index) in goodsCategories" :key="index" class="option-item"
+                :class="{ active: selectedCategory === index }" @click="selectCategory(index)">
+                <div class="option-icon">
+                  <img class="option-icon-img" :src="category.image" alt="category.name">
                 </div>
+                <span class="option-text">{{ category.name }}</span>
               </div>
             </div>
-  
-            <!-- 商品下的套餐列表 -->
-            <GoodsList :goods-list="goodsList" @handleClick="gotoBuy" />
           </div>
-        </el-card>
-      </div>
+
+          <!-- 商品下的套餐列表 -->
+          <GoodsList :goods-list="goodsList" @handleClick="gotoBuy" />
+        </div>
+      </el-card>
     </div>
-  </template>
-  
+  </div>
+</template>
+
 <script setup lang="ts">
-import { ref } from 'vue'
+import { onMounted, ref } from 'vue'
 import { Goods } from '@element-plus/icons-vue'
-  import { useRouter } from 'vue-router'
-  import Header from './components/header.vue'
-  import GoodsList from './components/goods.vue'
+import { useRouter } from 'vue-router'
+import Header from './components/header.vue'
+import GoodsList from './components/goods.vue'
+import { productList, list } from '/@/api/frontend/home'
 
-  type GoodsCategory = {
-    name: string
-    id: number
-  }
+type GoodsCategory = {
+  name: string
+  id: number
+  package_id:number
+  image:string
+}
 
-  type GoodsItem = {
-    id: string | number
-    img: string
-    name: string
-    price: number
-    sold: number
-    status: string
-    type: number // 类型 1.个人  2.企业
+type GoodsItem = {
+  id: string | number
+  image: string
+  name: string
+  price: number
+  price_text:string
+  sold_count: number
+  stock_text: string // 库存状态 充足 即将售罄 售罄
+  type: number // 类型 1.个人  2.企业
+  // 新卡类型结构:优先使用 card_info(接口可能仍返回 card_type 作为兼容字段)
+  card_type?: number
+  card_info?: {
+    name?: string
+    show_value?: string
   }
+}
+
+const router = useRouter()
 
-  const router = useRouter()
-
-  const selectedCategory = ref(0)
-  const goodsCategories = ref<GoodsCategory[]>([
-    { name: '棱镜', id: 1 },
-    { name: '聚德神笔', id: 2 }
-  ])
-  const goodsList = ref<GoodsItem[]>([
-    {
-      id: 1,
-      img: 'https://picsum.photos/200/300',
-      name: '惊鸿·日卡',
-      price: 5.00,
-      sold: 318,
-      status: '即将售罄',
-      type: 1 // 类型 1.个人  2.企业
-    },
-    {
-      id: 2,
-      img: 'https://picsum.photos/200',
-      name: '扶摇·周卡',
-      price: 23.00,
-      sold: 79,
-      status: '即将售罄',
-      type: 1 // 类型 1.个人  2.企业
-    },
-    {
-      id: 3,
-      img: 'https://picsum.photos/200/300',
-      name: '望舒·月卡',
-      price: 52.00,
-      sold: 265,
-      status: '一般',
-      type: 2 // 类型 1.个人  2.企业
-    },
-    {
-      id: 4,
-      img: 'https://picsum.photos/310',
-      name: '惊鸿·日卡',
-      price: 5.00,
-      sold: 318,
-      status: '即将售罄',
-      type: 1 // 类型 1.个人  2.企业
-    },
-    {
-      id: 5,
-      img: 'https://picsum.photos/200',
-      name: '扶摇·周卡',
-      price: 23.00,
-      sold: 79,
-      status: '即将售罄',
-      type: 2 // 类型 1.个人  2.企业
-    },
-    {
-      id: 6,
-      img: 'https://picsum.photos/300',
-      name: '望舒·月卡',
-      price: 52.00,
-      sold: 265,
-      status: '一般',
-      type: 1 // 类型 1.个人  2.企业
+const selectedCategory = ref(0) // 选中分类
+const goodsCategories = ref<GoodsCategory[]>([]) // 分类列表
+const goodsList = ref<GoodsItem[]>([]) // 套餐列表
+
+// 获取分类列表
+const getGoodsCategories = () => {
+  productList().then((res: any) => {
+    goodsCategories.value = res.data
+    getGoodsList(goodsCategories.value[selectedCategory.value].id)
+  })
+}
+
+// 获取套餐列表
+const getGoodsList = (id:number) => {
+  list({ product_id:id }).then((res: any) => {
+    goodsList.value = res.data
+  })
+}
+
+// 选择产品
+const selectCategory = (index: number) => {
+  selectedCategory.value = index
+  getGoodsList(goodsCategories.value[index].id)
+}
+
+// 去下单页面
+const gotoBuy = (item: GoodsItem) => {
+  router.push({
+    name: 'userOrder',
+    params: {
+      goodsId:item.id
     }
-  ])
+  })
+}
 
-  // 选择产品
-  const selectCategory = (index: number) => {
-    selectedCategory.value = index
-    // 这里可以触发商品列表的更新
-    // loadGoods(goodsCategories.value[index].id)
-  }
+// 初始化
+onMounted(() => {
+  getGoodsCategories()
+})
+</script>
 
-  // 去下单页面
-  const gotoBuy = (item: GoodsItem) => {
-    router.push({
-      name: 'userOrder',
-      params: {
-        goodsId: item.id
-      }
-    })
-  }
-  </script>
-  
-  <style scoped lang="scss">
-  .client-home {
-    min-height: 100vh;
-    background: #f5f7fa;
-    display: flex;
-    flex-direction: column;
-  
-    .home-goods {
-      width: 80%;
-      margin: 0 auto;
-      margin-top: 20px;
-  
-      .goods-list {
-        .goods-list-option {
-          margin-bottom: 24px;
-  
-          .option-scroll-wrapper {
-            display: flex;
-            gap: 12px;
-            overflow-x: auto;
-            overflow-y: hidden;
-            padding: 4px 0 12px 0;
-            scrollbar-width: thin;
-            scrollbar-color: rgba(64, 158, 255, 0.3) transparent;
-  
-            // 自定义滚动条样式(Webkit浏览器)
-            &::-webkit-scrollbar {
-              height: 6px;
-            }
-  
-            &::-webkit-scrollbar-track {
-              background: transparent;
-              border-radius: 3px;
-            }
-  
-            &::-webkit-scrollbar-thumb {
-              background: rgba(64, 158, 255, 0.3);
-              border-radius: 3px;
-  
-              &:hover {
-                background: rgba(64, 158, 255, 0.5);
-              }
+<style scoped lang="scss">
+.client-home {
+  min-height: 100vh;
+  background: #f5f7fa;
+  display: flex;
+  flex-direction: column;
+
+  .home-goods {
+    width: 80%;
+    margin: 0 auto;
+    margin-top: 20px;
+
+    .goods-list {
+      .goods-list-option {
+        margin-bottom: 24px;
+
+        .option-scroll-wrapper {
+          display: flex;
+          gap: 12px;
+          overflow-x: auto;
+          overflow-y: hidden;
+          padding: 4px 0 12px 0;
+          scrollbar-width: thin;
+          scrollbar-color: rgba(64, 158, 255, 0.3) transparent;
+
+          // 自定义滚动条样式(Webkit浏览器)
+          &::-webkit-scrollbar {
+            height: 6px;
+          }
+
+          &::-webkit-scrollbar-track {
+            background: transparent;
+            border-radius: 3px;
+          }
+
+          &::-webkit-scrollbar-thumb {
+            background: rgba(64, 158, 255, 0.3);
+            border-radius: 3px;
+
+            &:hover {
+              background: rgba(64, 158, 255, 0.5);
             }
-  
-            // 平滑滚动
-            scroll-behavior: smooth;
-  
-            .option-item {
+          }
+
+          // 平滑滚动
+          scroll-behavior: smooth;
+
+          .option-item {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            padding: 10px 20px;
+            background: #ffffff;
+            border: 1.5px solid #303133;
+            border-radius: 24px;
+            cursor: pointer;
+            transition: all 0.3s ease;
+            white-space: nowrap;
+            flex-shrink: 0;
+            user-select: none;
+
+            .option-icon {
+              width: 28px;
+              height: 28px;
+              border-radius: 50%;
               display: flex;
               align-items: center;
-              gap: 8px;
-              padding: 10px 20px;
-              background: #ffffff;
-              border: 1.5px solid #303133;
-              border-radius: 24px;
-              cursor: pointer;
-              transition: all 0.3s ease;
-              white-space: nowrap;
+              justify-content: center;
+              color: #ffffff;
               flex-shrink: 0;
-              user-select: none;
-  
-              .option-icon {
-                width: 28px;
-                height: 28px;
+              transition: all 0.3s ease;
+
+              .option-icon-img {
+                width: 30px;
+                height: 30px;
                 border-radius: 50%;
-                background: #303133;
-                display: flex;
-                align-items: center;
-                justify-content: center;
+              }
+            }
+
+            .option-text {
+              color: #303133;
+              font-size: 14px;
+              font-weight: 500;
+              letter-spacing: 0.3px;
+              transition: color 0.3s ease;
+            }
+
+            &:hover {
+              transform: translateY(-2px);
+              box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+              border-color: #606266;
+            }
+
+            // 激活状态:蓝色底色,白字
+            &.active {
+              background: #409eff;
+              border-color: #409eff;
+              box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+              transform: translateY(-2px);
+
+              .option-icon {
+                background: rgba(255, 255, 255, 0.2);
                 color: #ffffff;
-                flex-shrink: 0;
-                transition: all 0.3s ease;
-  
-                svg {
-                  width: 16px;
-                  height: 16px;
-                }
               }
-  
+
               .option-text {
-                color: #303133;
-                font-size: 14px;
-                font-weight: 500;
-                letter-spacing: 0.3px;
-                transition: color 0.3s ease;
+                color: #ffffff;
               }
-  
+
               &:hover {
-                transform: translateY(-2px);
-                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-                border-color: #606266;
-              }
-  
-              // 激活状态:蓝色底色,白字
-              &.active {
-                background: #409eff;
-                border-color: #409eff;
-                box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
-                transform: translateY(-2px);
-  
-                .option-icon {
-                  background: rgba(255, 255, 255, 0.2);
-                  color: #ffffff;
-                }
-  
-                .option-text {
-                  color: #ffffff;
-                }
-  
-                &:hover {
-                  background: #66b1ff;
-                  border-color: #66b1ff;
-                  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
-                }
-              }
-  
-              &:active {
-                transform: translateY(0);
+                background: #66b1ff;
+                border-color: #66b1ff;
+                box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
               }
             }
+
+            &:active {
+              transform: translateY(0);
+            }
           }
         }
       }
     }
-  
-    // 移动端样式
-    @media (max-width: 768px) {
-      .home-goods {
-        width: 100%;
-        padding: 0 16px;
-        margin-top: 16px;
-  
-        .goods-list {
-          .goods-list-option {
-            margin-bottom: 16px;
-  
-            .option-scroll-wrapper {
-              gap: 8px;
-              padding: 2px 0 8px 0;
-  
-              .option-item {
-                padding: 6px 14px;
-                border-radius: 18px;
-                border-width: 1px;
-                gap: 6px;
-  
-                .option-icon {
-                  width: 22px;
-                  height: 22px;
-  
-                  svg {
-                    width: 12px;
-                    height: 12px;
-                  }
-                }
-  
-                .option-text {
-                  font-size: 12px;
-                  letter-spacing: 0.2px;
-                }
-  
-                &:hover {
-                  transform: translateY(-1px);
-                }
-  
-                &.active {
-                  transform: translateY(-1px);
+  }
+
+  // 移动端样式
+  @media (max-width: 768px) {
+    .home-goods {
+      width: 100%;
+      padding: 0 16px;
+      margin-top: 16px;
+
+      .goods-list {
+        .goods-list-option {
+          margin-bottom: 16px;
+
+          .option-scroll-wrapper {
+            gap: 8px;
+            padding: 2px 0 8px 0;
+
+            .option-item {
+              padding: 6px 14px;
+              border-radius: 18px;
+              border-width: 1px;
+              gap: 6px;
+
+              .option-icon {
+                width: 22px;
+                height: 22px;
+
+                svg {
+                  width: 12px;
+                  height: 12px;
                 }
               }
+
+              .option-text {
+                font-size: 12px;
+                letter-spacing: 0.2px;
+              }
+
+              &:hover {
+                transform: translateY(-1px);
+              }
+
+              &.active {
+                transform: translateY(-1px);
+              }
             }
           }
         }
       }
     }
   }
-  </style>
-  
+}
+</style>

+ 346 - 366
web/src/views/frontend/user/login.vue

@@ -1,379 +1,359 @@
 <template>
-    <div class="client-login">
-      <div class="client-login__panel">
-        <h1 class="client-login__title">用户登录</h1>
-        <p class="client-login__subtitle">探索并购买前沿AI工具,赋能您的效率与创意</p>
-        <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="client-login__form" @submit.prevent>
-          <el-form-item prop="username">
-            <el-input 
-              v-model="loginForm.username" 
-              placeholder="请输入账号" 
-              size="large"
-              clearable
-            >
-              <template #prefix>
-                <Icon name="fa fa-user" size="16" color="var(--el-input-icon-color)" />
-              </template>
-            </el-input>
-          </el-form-item>
-          <el-form-item prop="password">
-            <el-input 
-              v-model="loginForm.password" 
-              type="password" 
-              placeholder="请输入密码"
-              size="large"
-              show-password 
-              @keyup.enter="handleLogin"
-            >
-              <template #prefix>
-                <Icon name="fa fa-unlock-alt" size="16" color="var(--el-input-icon-color)" />
-              </template>
-            </el-input>
-          </el-form-item>
-          <el-form-item>
-            <el-button 
-              :loading="loading" 
-              type="primary" 
-              class="client-login__button" 
-              size="large"
-              round
-              @click="handleLogin"
-            >
-              登录
-            </el-button>
-          </el-form-item>
-        </el-form>
-        <div class="client-login__links">
-          <router-link to="/user/reset">
-            忘记密码?
-          </router-link>
-        </div>
-        <div class="client-login__actions">
-          <span>还没有账号?</span>
-          <router-link to="/user/register">立即注册</router-link>
-        </div>
+  <div class="client-login">
+    <div class="client-login__panel">
+      <h1 class="client-login__title">用户登录</h1>
+      <p class="client-login__subtitle">探索并购买前沿AI工具,赋能您的效率与创意</p>
+      <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="client-login__form" @submit.prevent>
+        <el-form-item prop="username">
+          <el-input v-model="loginForm.username" placeholder="请输入用户名/邮箱" size="large" clearable>
+            <template #prefix>
+              <Icon name="fa fa-user" size="16" color="var(--el-input-icon-color)" />
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" size="large" show-password
+            @keyup.enter="handleLogin">
+            <template #prefix>
+              <Icon name="fa fa-unlock-alt" size="16" color="var(--el-input-icon-color)" />
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button :loading="loading" type="primary" class="client-login__button" size="large" round
+            @click="handleLogin">
+            登录
+          </el-button>
+        </el-form-item>
+      </el-form>
+      <div class="client-login__links">
+        <router-link to="/user/reset">
+          忘记密码?
+        </router-link>
+      </div>
+      <div class="client-login__actions">
+        <span>还没有账号?</span>
+        <router-link to="/user/register">立即注册</router-link>
       </div>
     </div>
-  </template>
-  
-  <script setup>
-  import { ref, reactive, watch, onMounted } from 'vue'
-  import { useRouter, useRoute } from 'vue-router'
-  import { useUserInfo } from '/@/stores/userInfo'
-  import { checkIn } from '/@/api/frontend/user/index'
-  import { uuid } from '/@/utils/random'
-  import clickCaptcha from '/@/components/clickCaptcha'
-  import Icon from '/@/components/icon/index.vue'
-  
-  defineOptions({
-    name: 'ClientLogin'
-  })
-  
-  const router = useRouter()
-  const route = useRoute()
-  const userInfo = useUserInfo()
-  
-  const loginFormRef = ref(null)
-  const loading = ref(false)
-  const redirect = ref(undefined)
-  const otherQuery = ref({})
-  
-  // 验证码开关
-  const userLoginCaptchaSwitch = ref(true)
-  
-  // 登录成功后要跳转的URL
-  const to = ref(Array.isArray(route.query.to) ? route.query.to[0] : (route.query.to || ''))
-  
-  const loginForm = reactive({
-    username: '',
-    password: '',
-    keep: false,
-    captchaId: uuid(),
-    captchaInfo: ''
-  })
-  
-  const validateUsername = (rule, value, callback) => {
-    if (!value) {
-      callback(new Error('请输入账号'))
-    } else {
-      callback()
-    }
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useUserInfo } from '/@/stores/userInfo'
+import { checkIn } from '/@/api/frontend/user/index'
+import { uuid } from '/@/utils/random'
+import clickCaptcha from '/@/components/clickCaptcha'
+import Icon from '/@/components/icon/index.vue'
+
+defineOptions({
+  name: 'ClientLogin'
+})
+
+const router = useRouter()
+const route = useRoute()
+const userInfo = useUserInfo()
+
+const loginFormRef = ref(null)
+const loading = ref(false)
+const redirect = ref(undefined)
+const otherQuery = ref({})
+
+// 验证码开关
+const userLoginCaptchaSwitch = ref(true)
+
+// 登录成功后要跳转的URL
+const to = ref(Array.isArray(route.query.to) ? route.query.to[0] : (route.query.to || ''))
+
+const loginForm = reactive({
+  username: '',
+  password: '',
+  keep: false,
+  captchaId: uuid(),
+  captchaInfo: ''
+})
+
+const validateUsername = (rule, value, callback) => {
+  if (!value) {
+    callback(new Error('请输入用户名/邮箱'))
+  } else {
+    callback()
+  }
+}
+
+const validatePassword = (rule, value, callback) => {
+  if (!value) {
+    callback(new Error('请输入密码'))
+  } else if (value.length < 6) {
+    callback(new Error('密码长度不能小于6位'))
+  } else {
+    callback()
   }
-  
-  const validatePassword = (rule, value, callback) => {
-    if (!value) {
-      callback(new Error('请输入密码'))
-    } else if (value.length < 6) {
-      callback(new Error('密码长度不能小于6位'))
+}
+
+const loginRules = reactive({
+  username: [{ required: true, trigger: 'blur', validator: validateUsername }],
+  password: [{ required: true, trigger: 'blur', validator: validatePassword }]
+})
+
+const getOtherQuery = (query) => {
+  return Object.keys(query).reduce((acc, cur) => {
+    if (cur !== 'redirect' && cur !== 'to') {
+      acc[cur] = query[cur]
+    }
+    return acc
+  }, {})
+}
+
+// 提交登录前的预处理(验证码检查)
+const handleLogin = () => {
+  if (!loginFormRef.value) return
+
+  loginFormRef.value.validate((valid) => {
+    if (!valid) return
+
+    // 如果启用了验证码,先进行验证码验证
+    if (userLoginCaptchaSwitch.value) {
+      clickCaptcha(loginForm.captchaId, (captchaInfo) => {
+        onSubmit(captchaInfo)
+      })
     } else {
-      callback()
+      onSubmit()
     }
-  }
-  
-  const loginRules = reactive({
-    username: [{ required: true, trigger: 'blur', validator: validateUsername }],
-    password: [{ required: true, trigger: 'blur', validator: validatePassword }]
   })
-  
-  const getOtherQuery = (query) => {
-    return Object.keys(query).reduce((acc, cur) => {
-      if (cur !== 'redirect' && cur !== 'to') {
-        acc[cur] = query[cur]
-      }
-      return acc
-    }, {})
-  }
-  
-  // 提交登录前的预处理(验证码检查)
-  const handleLogin = () => {
-    if (!loginFormRef.value) return
-    
-    loginFormRef.value.validate((valid) => {
-      if (!valid) return
-      
-      // 如果启用了验证码,先进行验证码验证
-      if (userLoginCaptchaSwitch.value) {
-        clickCaptcha(loginForm.captchaId, (captchaInfo) => {
-          onSubmit(captchaInfo)
-        })
-      } else {
-        onSubmit()
-      }
-    })
-  }
-  
-  // 提交登录
-  const onSubmit = (captchaInfo = '') => {
-    loading.value = true
-    loginForm.captchaInfo = captchaInfo
-    
-    checkIn('post', {
-      tab: 'login',
-      username: loginForm.username,
-      password: loginForm.password,
-      keep: loginForm.keep,
-      captchaId: loginForm.captchaId,
-      captchaInfo: loginForm.captchaInfo
-    }).then((res) => {
-      // 登录成功,更新用户信息
-      if (res.data && res.data.userInfo) {
-        userInfo.dataFill(res.data.userInfo, false)
-      }
-      
-      // 如果有指定的跳转URL,则跳转
-      if (to.value) {
-        location.href = to.value
-        return
-      }
-      
-      // 使用返回的routePath或redirect指定的页面
-      router.push({
-        path: res.data?.routePath || redirect.value || '/user/home',
-        query: otherQuery.value
-      })
-    }).catch(() => {
-      // 登录失败,错误信息由 axios 拦截器处理
-      // 如果验证失败,重新生成验证码ID
-      if (userLoginCaptchaSwitch.value) {
-        loginForm.captchaId = uuid()
-      }
-    }).finally(() => {
-      loading.value = false
-    })
-  }
-  
-  // 监听路由变化
-  watch(
-    () => route.query,
-    (query) => {
-      if (query) {
-        redirect.value = query.redirect
-        to.value = Array.isArray(query.to) ? query.to[0] : (query.to || '')
-        otherQuery.value = getOtherQuery(query)
-      }
-    },
-    { immediate: true }
-  )
-  
-  // 组件挂载时获取登录配置
-  onMounted(() => {
-    checkIn('get').then((res) => {
-      if (res.data) {
-        userLoginCaptchaSwitch.value = res.data.userLoginCaptchaSwitch !== undefined 
-          ? res.data.userLoginCaptchaSwitch 
-          : true
-      }
+}
+
+// 提交登录
+const onSubmit = (captchaInfo = '') => {
+  loading.value = true
+  loginForm.captchaInfo = captchaInfo
+
+  checkIn('post', {
+    tab: 'login',
+    username: loginForm.username,
+    password: loginForm.password,
+    keep: loginForm.keep,
+    captchaId: loginForm.captchaId,
+    captchaInfo: loginForm.captchaInfo
+  }).then((res) => {
+    // 登录成功,更新用户信息
+    if (res.data && res.data.userInfo) {
+      userInfo.dataFill(res.data.userInfo, false)
+    }
+
+    // 如果有指定的跳转URL,则跳转
+    if (to.value) {
+      location.href = to.value
+      return
+    }
+
+    // 使用返回的routePath或redirect指定的页面
+    router.push({
+      path: res.data?.routePath || redirect.value || '/',
+      query: otherQuery.value
     })
+  }).catch(() => {
+    // 登录失败,错误信息由 axios 拦截器处理
+    // 如果验证失败,重新生成验证码ID
+    if (userLoginCaptchaSwitch.value) {
+      loginForm.captchaId = uuid()
+    }
+  }).finally(() => {
+    loading.value = false
   })
-  </script>
-  
-  <style scoped>
-  .client-login {
-    min-height: 100vh;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
-    position: relative;
-  }
-  
-  .client-login::before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: rgba(255, 255, 255, 0.1);
-    backdrop-filter: blur(2px);
-  }
-  
+}
+
+// 监听路由变化
+watch(
+  () => route.query,
+  (query) => {
+    if (query) {
+      redirect.value = query.redirect
+      to.value = Array.isArray(query.to) ? query.to[0] : (query.to || '')
+      otherQuery.value = getOtherQuery(query)
+    }
+  },
+  { immediate: true }
+)
+
+// 组件挂载时获取登录配置
+onMounted(() => {
+  checkIn('get').then((res) => {
+    if (res.data) {
+      userLoginCaptchaSwitch.value = res.data.userLoginCaptchaSwitch !== undefined? res.data.userLoginCaptchaSwitch : true
+    }
+  })
+})
+</script>
+
+<style scoped>
+.client-login {
+  min-height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
+  position: relative;
+}
+
+.client-login::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.1);
+  backdrop-filter: blur(2px);
+}
+
+.client-login__panel {
+  width: 420px;
+  padding: 48px 40px 40px;
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.98);
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+  backdrop-filter: blur(10px);
+  position: relative;
+  z-index: 1;
+  transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.client-login__panel:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
+}
+
+.client-login__title {
+  margin: 0 0 8px 0;
+  font-size: 28px;
+  font-weight: 700;
+  text-align: center;
+  background: linear-gradient(135deg, #409eff 0%, #638fee 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  letter-spacing: 0.5px;
+}
+
+.client-login__subtitle {
+  margin: 0 0 32px 0;
+  font-size: 14px;
+  text-align: center;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.client-login__form {
+  margin-top: 8px;
+}
+
+.client-login__form :deep(.el-form-item) {
+  margin-bottom: 24px;
+}
+
+.client-login__form :deep(.el-input__wrapper) {
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+  transition: all 0.3s ease;
+}
+
+.client-login__form :deep(.el-input__wrapper:hover) {
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
+}
+
+.client-login__form :deep(.el-input.is-focus .el-input__wrapper) {
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.25);
+}
+
+.client-login__form :deep(.el-input__prefix) {
+  padding-left: 12px;
+}
+
+.client-login__actions {
+  margin-top: 20px;
+  font-size: 14px;
+  color: #606266;
+  text-align: center;
+}
+
+.client-login__actions a {
+  color: #409eff;
+  margin-left: 6px;
+  text-decoration: none;
+  transition: color 0.2s;
+  font-weight: 500;
+}
+
+.client-login__actions a:hover {
+  color: #66b1ff;
+}
+
+.client-login__button {
+  width: 100%;
+  height: 44px;
+  font-size: 16px;
+  font-weight: 600;
+  letter-spacing: 1px;
+  background: #409eff;
+  border: none;
+  transition: all 0.3s ease;
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+}
+
+.client-login__button:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
+}
+
+.client-login__button:active {
+  transform: translateY(0);
+}
+
+.client-login__links {
+  margin-top: 8px;
+  margin-bottom: 8px;
+  display: flex;
+  justify-content: flex-end;
+  gap: 6px;
+  font-size: 13px;
+  color: #909399;
+}
+
+.client-login__links a {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  color: #409eff;
+  text-decoration: none;
+  transition: all 0.2s;
+  font-weight: 500;
+}
+
+.client-login__links a:hover {
+  color: #66b1ff;
+  transform: translateX(2px);
+}
+
+.client-login__tips {
+  margin-top: 12px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.6;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
   .client-login__panel {
-    width: 420px;
-    padding: 48px 40px 40px;
-    border-radius: 16px;
-    background: rgba(255, 255, 255, 0.98);
-    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
-    backdrop-filter: blur(10px);
-    position: relative;
-    z-index: 1;
-    transition: transform 0.3s ease, box-shadow 0.3s ease;
-  }
-  
-  .client-login__panel:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
+    width: 90%;
+    max-width: 420px;
+    padding: 36px 28px 32px;
   }
-  
+
   .client-login__title {
-    margin: 0 0 8px 0;
-    font-size: 28px;
-    font-weight: 700;
-    text-align: center;
-    background: linear-gradient(135deg, #409eff 0%, #638fee 100%);
-    -webkit-background-clip: text;
-    -webkit-text-fill-color: transparent;
-    background-clip: text;
-    letter-spacing: 0.5px;
-  }
-  
-  .client-login__subtitle {
-    margin: 0 0 32px 0;
-    font-size: 14px;
-    text-align: center;
-    color: #909399;
-    line-height: 1.5;
-  }
-  
-  .client-login__form {
-    margin-top: 8px;
-  }
-  
-  .client-login__form :deep(.el-form-item) {
-    margin-bottom: 24px;
-  }
-  
-  .client-login__form :deep(.el-input__wrapper) {
-    border-radius: 8px;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
-    transition: all 0.3s ease;
-  }
-  
-  .client-login__form :deep(.el-input__wrapper:hover) {
-    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
-  }
-  
-  .client-login__form :deep(.el-input.is-focus .el-input__wrapper) {
-    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.25);
-  }
-  
-  .client-login__form :deep(.el-input__prefix) {
-    padding-left: 12px;
-  }
-  
-  .client-login__actions {
-    margin-top: 20px;
-    font-size: 14px;
-    color: #606266;
-    text-align: center;
-  }
-  
-  .client-login__actions a {
-    color: #409eff;
-    margin-left: 6px;
-    text-decoration: none;
-    transition: color 0.2s;
-    font-weight: 500;
-  }
-  
-  .client-login__actions a:hover {
-    color: #66b1ff;
-  }
-  
-  .client-login__button {
-    width: 100%;
-    height: 44px;
-    font-size: 16px;
-    font-weight: 600;
-    letter-spacing: 1px;
-    background: #409eff;
-    border: none;
-    transition: all 0.3s ease;
-    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
-  }
-  
-  .client-login__button:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
-  }
-  
-  .client-login__button:active {
-    transform: translateY(0);
-  }
-  
-  .client-login__links {
-    margin-top: 8px;
-    margin-bottom: 8px;
-    display: flex;
-    justify-content: flex-end;
-    gap: 6px;
-    font-size: 13px;
-    color: #909399;
-  }
-  
-  .client-login__links a {
-    display: inline-flex;
-    align-items: center;
-    gap: 4px;
-    color: #409eff;
-    text-decoration: none;
-    transition: all 0.2s;
-    font-weight: 500;
-  }
-  
-  .client-login__links a:hover {
-    color: #66b1ff;
-    transform: translateX(2px);
-  }
-  
-  .client-login__tips {
-    margin-top: 12px;
-    font-size: 12px;
-    color: #909399;
-    line-height: 1.6;
-  }
-  
-  /* 响应式设计 */
-  @media (max-width: 768px) {
-    .client-login__panel {
-      width: 90%;
-      max-width: 420px;
-      padding: 36px 28px 32px;
-    }
-    
-    .client-login__title {
-      font-size: 24px;
-    }
+    font-size: 24px;
   }
-  </style>
-  
+}
+</style>

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff