19 Commits c62a5550cc ... 61be33ce1f

Tác giả SHA1 Thông báo Ngày
  “陈博论” 61be33ce1f 订单页面修改 9 tháng trước cách đây
  “陈博论” 94926662de 支付宝下单 9 tháng trước cách đây
  “陈博论” f11328f434 套餐分配修改 9 tháng trước cách đây
  “陈博论” 0a5c108cc0 企业产品分配修改 9 tháng trước cách đây
  “陈博论” d7f28b90b2 产品分配逻辑修改 9 tháng trước cách đây
  PC-7\Administrator 82cc70d1dc 第一次大修改 9 tháng trước cách đây
  “陈博论” 8348b1c8df 忘记密码页面修改 9 tháng trước cách đây
  “陈博论” 27d0645c23 用户下单修改,判断有无企业 9 tháng trước cách đây
  “陈博论” ba273bf0d7 前端样式更新 9 tháng trước cách đây
  “陈博论” 5c5b034a0b 我的企业管理 9 tháng trước cách đây
  “陈博论” c63f96b477 我的企业编写 9 tháng trước cách đây
  “陈博论” f8e83144e3 我的下级列表编写 9 tháng trước cách đây
  “陈博论” 3b9419a7b4 个人中心信息编写 9 tháng trước cách đây
  “陈博论” 681195b448 订单支付页面编写 9 tháng trước cách đây
  “陈博论” c80909f840 客户端首页api对接 9 tháng trước cách đây
  “陈博论” c0e7be2d4a 登录注册修改 9 tháng trước cách đây
  “陈博论” 5e0e766b8d 前端样式修改 10 tháng trước cách đây
  “陈博论” 84ed9bcff4 前端打包更新 10 tháng trước cách đây
  “陈博论” c69dfcd770 前端修改 10 tháng trước cách đây
57 tập tin đã thay đổi với 12397 bổ sung và 2355 xóa
  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>
+

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác