Selaa lähdekoodia

Merge branch 'dev-rewardPoints' into dev

bianjunhui 4 päivää sitten
vanhempi
commit
1d0e0cc196
22 muutettua tiedostoa jossa 937 lisäystä ja 44 poistoa
  1. 167 25
      packages/Longyi/RewardPoints/src/Services/GrowthValueService.php
  2. 33 8
      packages/Longyi/RewardPoints/src/Services/LevelCalculationTrait.php
  3. 25 5
      packages/Webkul/Admin/src/DataGrids/Marketing/Promotions/CartRuleCouponDataGrid.php
  4. 23 2
      packages/Webkul/Admin/src/Http/Controllers/Marketing/Promotions/CartRuleController.php
  5. 11 2
      packages/Webkul/Admin/src/Http/Controllers/Marketing/Promotions/CartRuleCouponController.php
  6. 1 0
      packages/Webkul/Admin/src/Http/Requests/CartRuleRequest.php
  7. 5 0
      packages/Webkul/Admin/src/Resources/lang/en/app.php
  8. 5 0
      packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php
  9. 24 0
      packages/Webkul/Admin/src/Resources/views/marketing/promotions/cart-rules/create.blade.php
  10. 44 1
      packages/Webkul/Admin/src/Resources/views/marketing/promotions/cart-rules/edit.blade.php
  11. 24 0
      packages/Webkul/CartRule/src/Database/Migrations/2025_06_26_100000_add_customer_id_to_cart_rule_coupons.php
  12. 23 0
      packages/Webkul/CartRule/src/Database/Migrations/2025_06_26_150000_add_unique_code_to_cart_rule_coupons.php
  13. 7 0
      packages/Webkul/CartRule/src/Helpers/CartRule.php
  14. 10 1
      packages/Webkul/CartRule/src/Models/CartRuleCoupon.php
  15. 1 0
      packages/Webkul/CartRule/src/Repositories/CartRuleCouponRepository.php
  16. 3 0
      packages/Webkul/CartRule/src/Repositories/CartRuleRepository.php
  17. 11 0
      packages/Webkul/Shop/src/Http/Controllers/API/CartController.php
  18. 203 0
      packages/Webkul/Shop/src/Http/Controllers/API/CouponController.php
  19. 271 0
      packages/Webkul/Shop/src/Http/Controllers/API/OrderController.php
  20. 13 0
      packages/Webkul/Shop/src/Resources/lang/en/app.php
  21. 13 0
      packages/Webkul/Shop/src/Resources/lang/zh_CN/app.php
  22. 20 0
      packages/Webkul/Shop/src/Routes/api-token.php

+ 167 - 25
packages/Longyi/RewardPoints/src/Services/GrowthValueService.php

@@ -168,7 +168,10 @@ class GrowthValueService
      */
     public function checkAndUpdateLevel($customerId, $currentGrowthValue, $oldLevel = null): array
     {
-        $newLevel = $this->calculateLevel($currentGrowthValue);
+        $record = GrowthValueCustomer::where('customer_id', $customerId)->first();
+        $hasFirstOrder = $record && ! is_null($record->first_order_completed_at);
+
+        $newLevel = $this->calculateLevel($currentGrowthValue, $hasFirstOrder);
 
         if ($oldLevel === $newLevel) {
             return [
@@ -297,23 +300,137 @@ class GrowthValueService
     }
 
     /**
-     * 获取客户成长值信息
+     * 获取默认成长值信息
+     */
+    private function getDefaultGrowthValueInfo($customer): array
+    {
+        // 使用 calculateLevel 计算默认等级,传入 hasFirstOrder=false(无记录=未首单)
+        $currentLevelName = $this->calculateLevel(0, false);
+
+        // 获取当前等级对应的客户组
+        $currentGroup = CustomerGroup::where('growth_level_name', $currentLevelName)->first();
+
+        // 如果没有找到,使用 General 作为兜底
+        if (!$currentGroup) {
+            $currentGroup = CustomerGroup::where('code', 'general')->first();
+            $currentLevelName = $currentGroup->growth_level_name ?? 'General';
+        }
+
+        // 获取下一个等级(考虑首单要求)
+        $nextLevel = $this->getNextLevelForNewUser($currentLevelName);
+
+        // 计算到下一个等级需要的成长值
+        $pointsToNextLevel = null;
+        if ($nextLevel) {
+            $pointsToNextLevel = $nextLevel->min_growth_value - 0;
+
+            // 如果下一个等级需要首单,返回特殊提示
+            if ($nextLevel->require_first_order) {
+                $pointsToNextLevel = 'First order required';
+            }
+        }
+
+        return [
+            'growth_value' => 0,
+            'current_group' => $currentGroup,
+            'growth_level_name' => $currentLevelName,
+            'growth_discount_rate' => $currentGroup->growth_discount_rate ?? 0,
+            'growth_benefits' => $currentGroup->growth_benefits ?? [],
+            'growth_level_icon' => $currentGroup->growth_level_icon ?? null,
+            'next_level' => $nextLevel ? [
+                'group_id' => $nextLevel->id,
+                'group_name' => $nextLevel->name,
+                'growth_level_name' => $nextLevel->growth_level_name,
+                'min_growth_value' => $nextLevel->min_growth_value,
+                'icon' => $nextLevel->growth_level_icon,
+                'require_first_order' => (bool) $nextLevel->require_first_order,
+            ] : null,
+            'points_to_next_level' => $pointsToNextLevel,
+            'expires_at' => null,
+            'first_order_completed' => false,
+        ];
+    }
+
+    /**
+     * 获取新用户的下一个等级
+     *
+     * @param string $currentLevelName 当前等级名称
+     * @return \Webkul\Customer\Models\CustomerGroup|null
+     */
+    private function getNextLevelForNewUser(string $currentLevelName)
+    {
+        // 获取所有配置了成长值的等级(排除当前等级)
+        $allLevels = CustomerGroup::whereNotNull('growth_level_name')
+            ->where('growth_level_name', '!=', $currentLevelName)
+            ->orderBy('min_growth_value', 'asc')
+            ->orderBy('require_first_order', 'asc') // 先显示不需要首单的
+            ->get();
+
+        // 获取成长值为0的等级
+        $zeroValueLevels = $allLevels->filter(function ($level) {
+            return $level->min_growth_value == 0;
+        });
+
+        // 如果有成长值为0的等级,检查是否需要首单
+        if ($zeroValueLevels->isNotEmpty()) {
+            // 先找不需要首单的(除了当前等级)
+            $availableLevel = $zeroValueLevels->firstWhere('require_first_order', false);
+            if ($availableLevel) {
+                return $availableLevel;
+            }
+
+            // 如果没有不需要首单的,找需要首单的
+            $firstOrderLevel = $zeroValueLevels->firstWhere('require_first_order', true);
+            if ($firstOrderLevel) {
+                return $firstOrderLevel;
+            }
+        }
+
+        // 获取成长值 > 0 的等级
+        $higherLevels = $allLevels->filter(function ($level) {
+            return $level->min_growth_value > 0;
+        })->sortBy('min_growth_value');
+
+        // 如果成长值为0的等级都不存在,返回成长值 > 0 的最低等级
+        if ($higherLevels->isNotEmpty()) {
+            return $higherLevels->first();
+        }
+
+        return null;
+    }
+
+    /**
+     * 获取客户成长值信息(有记录的用户)
      */
     public function getCustomerGrowthValue($customerId): array
     {
         $growthValueCustomer = GrowthValueCustomer::where('customer_id', $customerId)->first();
         $customer = Customer::with('group')->find($customerId);
 
+        // 如果没有成长值记录,返回默认信息
         if (!$growthValueCustomer) {
             return $this->getDefaultGrowthValueInfo($customer);
         }
 
         $currentGroup = $customer->group;
 
-        // 获取下一个等级
-        $nextGroup = CustomerGroup::where('min_growth_value', '>', $growthValueCustomer->growth_value)
-            ->orderBy('min_growth_value', 'asc')
-            ->first();
+        // 检查用户是否已完成首单
+        $hasFirstOrder = !is_null($growthValueCustomer->first_order_completed_at);
+        $currentGrowthValue = $growthValueCustomer->growth_value;
+
+        // 获取下一个等级(考虑首单要求)
+        $nextGroup = $this->getNextLevelForUser($currentGrowthValue, $hasFirstOrder, $currentGroup->growth_level_name ?? null);
+
+        // 计算到下一个等级需要的成长值
+        $pointsToNextLevel = null;
+        if ($nextGroup) {
+            $pointsToNextLevel = $nextGroup->min_growth_value - $currentGrowthValue;
+
+            // 如果下一个等级需要首单但用户未完成,返回特殊提示
+            if ($nextGroup->require_first_order && !$hasFirstOrder) {
+                $pointsToNextLevel = '需要完成首单';
+            }
+        }
 
         return [
             'growth_value' => $growthValueCustomer->growth_value,
@@ -328,36 +445,61 @@ class GrowthValueService
                 'growth_level_name' => $nextGroup->growth_level_name,
                 'min_growth_value' => $nextGroup->min_growth_value,
                 'icon' => $nextGroup->growth_level_icon,
+                'require_first_order' => (bool) $nextGroup->require_first_order,
             ] : null,
-            'points_to_next_level' => $nextGroup
-                ? max(0, $nextGroup->min_growth_value - $growthValueCustomer->growth_value)
-                : null,
+            'points_to_next_level' => $pointsToNextLevel,
             'expires_at' => $growthValueCustomer->growth_value_expires_at,
-            'first_order_completed' => !is_null($growthValueCustomer->first_order_completed_at),
+            'first_order_completed' => $hasFirstOrder,
         ];
     }
 
     /**
-     * 获取默认成长值信息
+     * 获取用户的下一个等级
+     *
+     * @param int $currentGrowthValue 当前成长值
+     * @param bool $hasFirstOrder 是否已完成首单
+     * @param string|null $currentLevelName 当前等级名称
+     * @return \Webkul\Customer\Models\CustomerGroup|null
      */
-    private function getDefaultGrowthValueInfo($customer): array
+    private function getNextLevelForUser(int $currentGrowthValue, bool $hasFirstOrder, ?string $currentLevelName)
     {
-        $defaultGroup = CustomerGroup::where('code', 'general')->first();
+        // 获取所有成长值 >= 当前值的等级(排除当前等级)
+        $query = CustomerGroup::whereNotNull('growth_level_name')
+            ->where('min_growth_value', '>=', $currentGrowthValue);
 
-        return [
-            'growth_value' => 0,
-            'current_group' => $defaultGroup,
-            'growth_level_name' => $defaultGroup->growth_level_name ?? 'V0',
-            'growth_discount_rate' => $defaultGroup->growth_discount_rate ?? 0,
-            'growth_benefits' => $defaultGroup->growth_benefits ?? [],
-            'growth_level_icon' => $defaultGroup->growth_level_icon ?? null,
-            'next_level' => null,
-            'points_to_next_level' => null,
-            'expires_at' => null,
-            'first_order_completed' => false,
-        ];
+        // 如果当前等级名称存在,排除当前等级
+        if ($currentLevelName) {
+            $query->where('growth_level_name', '!=', $currentLevelName);
+        }
+
+        // 未完成首单时,排除需要首单的等级(但如果是成长值为0且需要首单的V1,要特殊处理)
+        if (!$hasFirstOrder) {
+            // 获取所有等级
+            $allLevels = $query->get();
+
+            // 过滤:如果是成长值为0且需要首单的等级,保留(因为可以作为next_level显示提示)
+            // 其他需要首单的等级,排除
+            $filteredLevels = $allLevels->filter(function ($level) {
+                if ($level->min_growth_value == 0 && $level->require_first_order) {
+                    return true; // 保留成长值为0且需要首单的等级(如V1)
+                }
+                return !$level->require_first_order; // 其他等级排除需要首单的
+            });
+
+            // 按成长值升序排列
+            $sortedLevels = $filteredLevels->sortBy('min_growth_value');
+
+            // 返回第一个等级
+            return $sortedLevels->first();
+        }
+
+        // 已完成首单,正常获取下一个等级
+        $nextLevel = $query->orderBy('min_growth_value', 'asc')->first();
+
+        return $nextLevel;
     }
 
+
     /**
      * 手动调整成长值
      */

+ 33 - 8
packages/Longyi/RewardPoints/src/Services/LevelCalculationTrait.php

@@ -6,17 +6,42 @@ use Webkul\Customer\Models\CustomerGroup;
 trait LevelCalculationTrait
 {
     /**
-     * 根据成长值计算等级
+     * 根据成长值计算等级,并考虑首单要求。
+     *
+     * @param int  $growthValue   当前成长值
+     * @param bool $hasFirstOrder 是否已完成首单
      */
-    protected function calculateLevel($growthValue)
+    protected function calculateLevel($growthValue, bool $hasFirstOrder = false): string
     {
-        $level = CustomerGroup::where('min_growth_value', '<=', $growthValue)
-            ->orderBy('min_growth_value', 'desc')
-            ->first();
+        // 获取所有成长值 <= 当前值的等级
+        $query = CustomerGroup::where('min_growth_value', '<=', $growthValue);
+
+        // 未完成首单时,排除需要首单的等级
+        if (! $hasFirstOrder) {
+            $query->where('require_first_order', false);
+        }
+
+        // 按成长值降序排列,取最高等级
+        $level = $query->orderBy('min_growth_value', 'desc')->first();
+
+        if ($level && $level->growth_level_name) {
+            return $level->growth_level_name;
+        }
+
+        // 如果没有找到,查找是否有成长值为0且不需要首单的等级
+        if (!$hasFirstOrder) {
+            $fallbackLevel = CustomerGroup::where('min_growth_value', 0)
+                ->where('require_first_order', false)
+                ->first();
+
+            if ($fallbackLevel && $fallbackLevel->growth_level_name) {
+                return $fallbackLevel->growth_level_name;
+            }
+        }
 
-        return $level && $level->growth_level_name
-            ? $level->growth_level_name
-            : 'V1';
+        // 兜底:返回 General 等级的名称
+        $general = CustomerGroup::where('code', 'general')->first();
+        return $general->growth_level_name ?? 'General';
     }
 
     /**

+ 25 - 5
packages/Webkul/Admin/src/DataGrids/Marketing/Promotions/CartRuleCouponDataGrid.php

@@ -15,12 +15,16 @@ class CartRuleCouponDataGrid extends DataGrid
     public function prepareQueryBuilder()
     {
         $queryBuilder = DB::table('cart_rule_coupons')
+            ->leftJoin('customers', 'cart_rule_coupons.customer_id', '=', 'customers.id')
             ->select(
-                'id',
-                'code',
-                'created_at',
-                'expired_at',
-                'times_used'
+                'cart_rule_coupons.id',
+                'cart_rule_coupons.code',
+                'cart_rule_coupons.created_at',
+                'cart_rule_coupons.expired_at',
+                'cart_rule_coupons.times_used',
+                'customers.email as customer_email',
+                'customers.first_name',
+                'customers.last_name'
             )
             ->where('cart_rule_coupons.cart_rule_id', request('id'));
 
@@ -77,6 +81,22 @@ class CartRuleCouponDataGrid extends DataGrid
             'filterable' => true,
             'sortable'   => true,
         ]);
+
+        $this->addColumn([
+            'index'      => 'customer_email',
+            'label'      => trans('admin::app.marketing.promotions.cart-rules-coupons.datagrid.customer'),
+            'type'       => 'string',
+            'searchable' => true,
+            'filterable' => true,
+            'sortable'   => true,
+            'closure'    => function ($row) {
+                if ($row->customer_email) {
+                    return $row->first_name.' '.$row->last_name.' ('.$row->customer_email.')';
+                }
+
+                return '-';
+            },
+        ]);
     }
 
     /**

+ 23 - 2
packages/Webkul/Admin/src/Http/Controllers/Marketing/Promotions/CartRuleController.php

@@ -82,9 +82,11 @@ class CartRuleController extends Controller
     public function store(CartRuleRequest $cartRuleRequest)
     {
         try {
+            $data = $this->resolveCustomerId($cartRuleRequest->all());
+
             Event::dispatch('promotions.cart_rule.create.before');
 
-            $cartRule = $this->cartRuleRepository->create($cartRuleRequest->all());
+            $cartRule = $this->cartRuleRepository->create($data);
 
             Event::dispatch('promotions.cart_rule.create.after', $cartRule);
 
@@ -120,6 +122,8 @@ class CartRuleController extends Controller
     public function update(CartRuleRequest $cartRuleRequest, int $id)
     {
         try {
+            $data = $this->resolveCustomerId($cartRuleRequest->all());
+
             $cartRule = $this->cartRuleRepository->findOrFail($id);
 
             if ($cartRule->coupon_type) {
@@ -136,7 +140,7 @@ class CartRuleController extends Controller
 
             Event::dispatch('promotions.cart_rule.update.before', $id);
 
-            $cartRule = $this->cartRuleRepository->update($cartRuleRequest->all(), $id);
+            $cartRule = $this->cartRuleRepository->update($data, $id);
 
             Event::dispatch('promotions.cart_rule.update.after', $cartRule);
 
@@ -152,6 +156,23 @@ class CartRuleController extends Controller
         return redirect()->back();
     }
 
+    /**
+     * Resolve customer email to customer_id.
+     */
+    private function resolveCustomerId(array $data): array
+    {
+        if (! empty($data['specific_customer_email'])) {
+            $customer = app('Webkul\Customer\Repositories\CustomerRepository')
+                ->findOneByField('email', $data['specific_customer_email']);
+            $data['customer_id'] = $customer ? $customer->id : null;
+        } else {
+            $data['customer_id'] = null;
+        }
+        unset($data['specific_customer_email']);
+
+        return $data;
+    }
+
     /**
      * Remove the specified resource from storage.
      */

+ 11 - 2
packages/Webkul/Admin/src/Http/Controllers/Marketing/Promotions/CartRuleCouponController.php

@@ -47,13 +47,22 @@ class CartRuleCouponController extends Controller
             ], 400);
         }
 
-        $this->cartRuleCouponRepository->generateCoupons(request()->only(
+        $data = request()->only(
             'coupon_qty',
             'code_length',
             'code_format',
             'code_prefix',
             'code_suffix'
-        ), $id);
+        );
+
+        // Resolve customer email to customer_id
+        if (request()->filled('specific_customer_email')) {
+            $customer = app('Webkul\Customer\Repositories\CustomerRepository')
+                ->findOneByField('email', request()->input('specific_customer_email'));
+            $data['customer_id'] = $customer ? $customer->id : null;
+        }
+
+        $this->cartRuleCouponRepository->generateCoupons($data, $id);
 
         return new JsonResponse([
             'message' => trans(

+ 1 - 0
packages/Webkul/Admin/src/Http/Requests/CartRuleRequest.php

@@ -33,6 +33,7 @@ class CartRuleRequest extends FormRequest
             'ends_till'           => 'nullable|date|after_or_equal:starts_from',
             'action_type'         => 'required',
             'discount_amount'     => 'required|numeric',
+            'customer_id'         => 'nullable|integer|exists:customers,id',
         ];
 
         if (! request('id')) {

+ 5 - 0
packages/Webkul/Admin/src/Resources/lang/en/app.php

@@ -2335,6 +2335,8 @@ return [
                     'uses-per-coupon'                           => 'Uses Per Coupon',
                     'uses-per-customer'                         => 'Uses Per Customer',
                     'uses-per-customer-control-info'            => 'Will be used for logged in customers only.',
+                    'specific-customer'                         => 'Specific Customer',
+                    'specific-customer-placeholder'             => 'Enter customer email to assign',
                     'yes'                                       => 'Yes',
                 ],
 
@@ -2420,6 +2422,8 @@ return [
                     'uses-per-coupon'                           => 'Uses Per Coupon',
                     'uses-per-customer'                         => 'Uses Per Customer',
                     'uses-per-customer-control-info'            => 'Will be used for logged in customers only.',
+                    'specific-customer'                         => 'Specific Customer',
+                    'specific-customer-placeholder'             => 'Enter customer email to assign',
                     'yes'                                       => 'Yes',
                 ],
 
@@ -2545,6 +2549,7 @@ return [
                 'datagrid' => [
                     'coupon-code'     => 'Coupon Code',
                     'created-date'    => 'Created Date',
+                    'customer'        => 'Customer',
                     'delete'          => 'Delete',
                     'expiration-date' => 'Expiration Date',
                     'id'              => 'ID',

+ 5 - 0
packages/Webkul/Admin/src/Resources/lang/zh_CN/app.php

@@ -2335,6 +2335,8 @@ return [
                     'uses-per-coupon'                           => '每张优惠券使用次数',
                     'uses-per-customer'                         => '每位顾客使用次数',
                     'uses-per-customer-control-info'            => '仅适用于已登录的顾客。',
+                    'specific-customer'                         => '指定专属顾客',
+                    'specific-customer-placeholder'             => '输入顾客邮箱进行指定',
                     'yes'                                       => '是',
                 ],
 
@@ -2420,6 +2422,8 @@ return [
                     'uses-per-coupon'                           => '每张优惠券使用次数',
                     'uses-per-customer'                         => '每位顾客使用次数',
                     'uses-per-customer-control-info'            => '仅适用于已登录的顾客。',
+                    'specific-customer'                         => '指定专属顾客',
+                    'specific-customer-placeholder'             => '输入顾客邮箱进行指定',
                     'yes'                                       => '是',
                 ],
 
@@ -2545,6 +2549,7 @@ return [
                 'datagrid' => [
                     'coupon-code'     => '优惠券代码',
                     'created-date'    => '创建日期',
+                    'customer'        => '指定顾客',
                     'delete'          => '删除',
                     'expiration-date' => '到期日期',
                     'id'              => 'ID',

+ 24 - 0
packages/Webkul/Admin/src/Resources/views/marketing/promotions/cart-rules/create.blade.php

@@ -195,6 +195,26 @@
 
                                 <x-admin::form.control-group.error control-name="uses_per_coupon" />
                             </x-admin::form.control-group>
+
+                            <!-- Specific Customer -->
+                            <x-admin::form.control-group>
+                                <x-admin::form.control-group.label>
+                                    @lang('admin::app.marketing.promotions.cart-rules.create.specific-customer')
+                                </x-admin::form.control-group.label>
+
+                                <x-admin::form.control-group.control
+                                    type="text"
+                                    id="specific_customer_email"
+                                    name="specific_customer_email"
+                                    :label="trans('admin::app.marketing.promotions.cart-rules.create.specific-customer')"
+                                    :placeholder="trans('admin::app.marketing.promotions.cart-rules.create.specific-customer-placeholder')"
+                                    v-model="specificCustomerEmail"
+                                />
+
+                                <input type="hidden" name="customer_id" :value="customerId" />
+
+                                <x-admin::form.control-group.error control-name="customer_id" />
+                            </x-admin::form.control-group>
                         </template>
 
                         <!-- Uses Per Customer -->
@@ -688,6 +708,10 @@
                         conditions: [],
 
                         actionType: "{{ old('action_type') ?: 'by_percent' }}",
+
+                        specificCustomerEmail: "{{ old('specific_customer_email', '') }}",
+
+                        customerId: "{{ old('customer_id', '') }}",
                     }
                 },
 

+ 44 - 1
packages/Webkul/Admin/src/Resources/views/marketing/promotions/cart-rules/edit.blade.php

@@ -213,6 +213,27 @@
 
                                 <x-admin::form.control-group.error control-name="uses_per_coupon" />
                             </x-admin::form.control-group>
+
+                            <!-- Specific Customer -->
+                            <x-admin::form.control-group>
+                                <x-admin::form.control-group.label>
+                                    @lang('admin::app.marketing.promotions.cart-rules.edit.specific-customer')
+                                </x-admin::form.control-group.label>
+
+                                <x-admin::form.control-group.control
+                                    type="text"
+                                    id="specific_customer_email"
+                                    name="specific_customer_email"
+                                    :value="old('specific_customer_email') ?? $cartRule->cart_rule_coupon?->customer?->email"
+                                    :label="trans('admin::app.marketing.promotions.cart-rules.edit.specific-customer')"
+                                    :placeholder="trans('admin::app.marketing.promotions.cart-rules.edit.specific-customer-placeholder')"
+                                    v-model="specificCustomerEmail"
+                                />
+
+                                <input type="hidden" name="customer_id" :value="customerId" />
+
+                                <x-admin::form.control-group.error control-name="customer_id" />
+                            </x-admin::form.control-group>
                         </template>
 
                         <!-- Usage Per Customer -->
@@ -738,6 +759,10 @@
                         conditions: @json($cartRule->conditions ?? []),
 
                         actionType: "{{ old('action_type') ?? $cartRule->action_type }}",
+
+                        specificCustomerEmail: "{{ old('specific_customer_email') ?? $cartRule->cart_rule_coupon?->customer?->email }}",
+
+                        customerId: "{{ old('customer_id') ?? $cartRule->cart_rule_coupon?->customer_id }}",
                     }
                 },
 
@@ -1238,6 +1263,22 @@
                                 </div>
                             </div>
 
+                            <!-- Specific Customer Email -->
+                            <x-admin::form.control-group>
+                                <x-admin::form.control-group.label>
+                                    @lang('admin::app.marketing.promotions.cart-rules.edit.specific-customer')
+                                </x-admin::form.control-group.label>
+
+                                <x-admin::form.control-group.control
+                                    type="text"
+                                    id="specific_customer_email"
+                                    name="specific_customer_email"
+                                    v-model="coupon_format.specific_customer_email"
+                                    :label="trans('admin::app.marketing.promotions.cart-rules.edit.specific-customer')"
+                                    :placeholder="trans('admin::app.marketing.promotions.cart-rules.edit.specific-customer-placeholder')"
+                                />
+                            </x-admin::form.control-group>
+
                             <div class="flex gap-4 max-sm:flex-wrap">
                                 <div class="mb-2.5 w-full">
                                     <!-- Code Format -->
@@ -1349,7 +1390,9 @@
 
                             code_prefix: '',
 
-                            code_suffix: ''
+                            code_suffix: '',
+
+                            specific_customer_email: '',
                         }
                     };
                 },

+ 24 - 0
packages/Webkul/CartRule/src/Database/Migrations/2025_06_26_100000_add_customer_id_to_cart_rule_coupons.php

@@ -0,0 +1,24 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('cart_rule_coupons', function (Blueprint $table) {
+            $table->integer('customer_id')->unsigned()->nullable()->after('expired_at');
+            $table->foreign('customer_id')->references('id')->on('customers')->onDelete('set null');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('cart_rule_coupons', function (Blueprint $table) {
+            $table->dropForeign(['customer_id']);
+            $table->dropColumn('customer_id');
+        });
+    }
+};

+ 23 - 0
packages/Webkul/CartRule/src/Database/Migrations/2025_06_26_150000_add_unique_code_to_cart_rule_coupons.php

@@ -0,0 +1,23 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        // MySQL: NULL 值在 UNIQUE 索引中允许重复,只有非 NULL 值必须唯一
+        Schema::table('cart_rule_coupons', function (Blueprint $table) {
+            $table->unique('code');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('cart_rule_coupons', function (Blueprint $table) {
+            $table->dropUnique(['code']);
+        });
+    }
+};

+ 7 - 0
packages/Webkul/CartRule/src/Helpers/CartRule.php

@@ -145,6 +145,13 @@ class CartRule
                     return false;
                 }
 
+                // Check if coupon is restricted to a specific customer
+                if ($coupon->customer_id) {
+                    if (! $this->cart->customer_id || $this->cart->customer_id != $coupon->customer_id) {
+                        return false;
+                    }
+                }
+
                 if (
                     $this->cart->customer_id
                     && $coupon->usage_per_customer

+ 10 - 1
packages/Webkul/CartRule/src/Models/CartRuleCoupon.php

@@ -22,6 +22,7 @@ class CartRuleCoupon extends Model implements CartRuleCouponContract
         'cart_rule_id',
         'expired_at',
         'is_primary',
+        'customer_id',
     ];
 
     /**
@@ -33,7 +34,15 @@ class CartRuleCoupon extends Model implements CartRuleCouponContract
     }
 
     /**
-     * Get the cart rule that owns the cart rule coupon.
+     * Get the customer assigned to this coupon.
+     */
+    public function customer(): BelongsTo
+    {
+        return $this->belongsTo(\Webkul\Customer\Models\CustomerProxy::modelClass());
+    }
+
+    /**
+     * Get the coupon usage records.
      */
     public function coupon_usage()
     {

+ 1 - 0
packages/Webkul/CartRule/src/Repositories/CartRuleCouponRepository.php

@@ -38,6 +38,7 @@ class CartRuleCouponRepository extends Repository
                 'usage_per_customer' => $cartRule->usage_per_customer ?? 0,
                 'is_primary'         => 0,
                 'expired_at'         => $cartRule->ends_till ?: null,
+                'customer_id'        => $data['customer_id'] ?? null,
             ]);
         }
     }

+ 3 - 0
packages/Webkul/CartRule/src/Repositories/CartRuleRepository.php

@@ -68,6 +68,7 @@ class CartRuleRepository extends Repository
                 'usage_per_customer' => $data['usage_per_customer'] ?? 0,
                 'is_primary'         => 1,
                 'expired_at'         => $data['ends_till'] ?? null,
+                'customer_id'        => $data['customer_id'] ?? null,
             ]);
         }
 
@@ -110,6 +111,7 @@ class CartRuleRepository extends Repository
                         'usage_limit'        => $data['uses_per_coupon'] ?? 0,
                         'usage_per_customer' => $data['usage_per_customer'] ?? 0,
                         'expired_at'         => $data['ends_till'] ?? null,
+                        'customer_id'        => $data['customer_id'] ?? null,
                     ], $cartRuleCoupon->id);
                 } else {
                     $this->cartRuleCouponRepository->create([
@@ -119,6 +121,7 @@ class CartRuleRepository extends Repository
                         'usage_per_customer' => $data['usage_per_customer'] ?? 0,
                         'is_primary'         => 1,
                         'expired_at'         => $data['ends_till'] ?? null,
+                        'customer_id'        => $data['customer_id'] ?? null,
                     ]);
                 }
             } else {

+ 11 - 0
packages/Webkul/Shop/src/Http/Controllers/API/CartController.php

@@ -220,6 +220,17 @@ class CartController extends APIController
                     ]))->response()->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
                 }
 
+                // Check if coupon is restricted to a specific customer
+                if ($coupon->customer_id) {
+                    $cart = Cart::getCart();
+                    if (! $cart || ! $cart->customer_id || $cart->customer_id != $coupon->customer_id) {
+                        return (new JsonResource([
+                            'data'     => new CartResource(Cart::getCart()),
+                            'message'  => trans('shop::app.checkout.coupon.invalid'),
+                        ]))->response()->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
+                    }
+                }
+
                 if ($coupon->cart_rule->status) {
                     if (Cart::getCart()->coupon_code == $coupon->code) {
                         return (new JsonResource([

+ 203 - 0
packages/Webkul/Shop/src/Http/Controllers/API/CouponController.php

@@ -0,0 +1,203 @@
+<?php
+
+namespace Webkul\Shop\Http\Controllers\API;
+
+use Carbon\Carbon;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\DB;
+use Longyi\RewardPoints\Helpers\ApiResponse;
+use Webkul\CartRule\Repositories\CartRuleCouponRepository;
+use Webkul\CartRule\Repositories\CartRuleRepository;
+
+class CouponController extends APIController
+{
+    protected string $charset = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
+
+    protected int $codeLength = 8;
+
+    public function __construct(
+        protected CartRuleCouponRepository $cartRuleCouponRepository,
+        protected CartRuleRepository $cartRuleRepository
+    ) {}
+
+    /**
+     * List all coupons for the current customer.
+     */
+    public function index(): JsonResponse
+    {
+        $customer = auth()->guard('customer')->user();
+        if (! $customer) {
+            return ApiResponse::unauthorized();
+        }
+
+        $coupons = DB::table('cart_rule_coupons')
+            ->join('cart_rules', 'cart_rule_coupons.cart_rule_id', '=', 'cart_rules.id')
+            ->leftJoin('cart_rule_coupon_usage', function ($join) use ($customer) {
+                $join->on('cart_rule_coupons.id', '=', 'cart_rule_coupon_usage.cart_rule_coupon_id')
+                    ->where('cart_rule_coupon_usage.customer_id', '=', $customer->id);
+            })
+            ->where('cart_rule_coupons.customer_id', $customer->id)
+            ->where('cart_rules.status', 1)
+            ->select(
+                'cart_rule_coupons.id',
+                'cart_rule_coupons.code',
+                'cart_rule_coupons.cart_rule_id',
+                'cart_rule_coupons.expired_at',
+                'cart_rule_coupons.usage_limit',
+                'cart_rule_coupons.usage_per_customer',
+                'cart_rule_coupons.times_used',
+                'cart_rule_coupons.created_at',
+                'cart_rules.name as rule_name',
+                'cart_rules.description as rule_description',
+                'cart_rules.action_type',
+                'cart_rules.discount_amount',
+                'cart_rules.ends_till',
+                'cart_rules.starts_from',
+                'cart_rule_coupon_usage.times_used as my_used'
+            )
+            ->orderBy('cart_rule_coupons.created_at', 'desc')
+            ->get()
+            ->map(function ($coupon) {
+                $now = Carbon::now();
+                $status = 'available';
+
+                if ($coupon->expired_at && Carbon::parse($coupon->expired_at)->lt($now)) {
+                    $status = 'expired';
+                } elseif ($coupon->ends_till && Carbon::parse($coupon->ends_till)->lt($now)) {
+                    $status = 'expired';
+                } elseif ($coupon->starts_from && Carbon::parse($coupon->starts_from)->gt($now)) {
+                    $status = 'not_started';
+                } elseif ($coupon->usage_limit && $coupon->times_used >= $coupon->usage_limit) {
+                    $status = 'exhausted';
+                } elseif ($coupon->usage_per_customer && $coupon->my_used >= $coupon->usage_per_customer) {
+                    $status = 'used';
+                }
+
+                $discountText = match ($coupon->action_type) {
+                    'by_percent' => floor($coupon->discount_amount).'%',
+                    'by_fixed'   => '$'.floor($coupon->discount_amount),
+                    'cart_fixed' => '$'.floor($coupon->discount_amount).' (whole cart)',
+                    'buy_x_get_y' => 'Buy X Get Y Free',
+                    default      => $coupon->discount_amount,
+                };
+
+                return [
+                    'id'              => $coupon->id,
+                    'code'            => $coupon->code,
+                    'cart_rule_id'    => $coupon->cart_rule_id,
+                    'rule_name'       => $coupon->rule_name,
+                    'rule_description'=> $coupon->rule_description,
+                    'discount_text'   => $discountText,
+                    'status'          => $status,
+                    'expired_at'      => $coupon->expired_at,
+                    'starts_from'     => $coupon->starts_from,
+                    'ends_till'       => $coupon->ends_till,
+                    'created_at'      => $coupon->created_at,
+                ];
+            });
+
+        return ApiResponse::success($coupons);
+    }
+
+    /**
+     * Claim a coupon by rule ID. Auto-generates a short code and binds to the current customer.
+     */
+    public function claim(): JsonResponse
+    {
+        try {
+            $customer = auth()->guard('customer')->user();
+            if (! $customer) {
+                return ApiResponse::unauthorized();
+            }
+
+            $validated = request()->validate([
+                'rule_id' => 'required|integer|exists:cart_rules,id',
+            ]);
+
+            $rule = $this->cartRuleRepository->find($validated['rule_id']);
+            if (! $rule) {
+                return ApiResponse::notFound('Cart rule not found');
+            }
+            if (! $rule->status) {
+                return ApiResponse::error('This coupon rule is currently inactive');
+            }
+            if ($rule->starts_from && Carbon::parse($rule->starts_from)->gt(Carbon::now())) {
+                return ApiResponse::error('This coupon rule has not started yet');
+            }
+            if ($rule->ends_till && Carbon::parse($rule->ends_till)->lt(Carbon::now())) {
+                return ApiResponse::error('This coupon rule has expired');
+            }
+            if (! $rule->coupon_type) {
+                return ApiResponse::error('This rule does not require a coupon code');
+            }
+
+            $existing = $this->cartRuleCouponRepository->findOneWhere([
+                'cart_rule_id' => $rule->id,
+                'customer_id'  => $customer->id,
+            ]);
+            if ($existing) {
+                return ApiResponse::success([
+                    'id'              => $existing->id,
+                    'code'            => $existing->code,
+                    'rule_name'       => $rule->name,
+                    'action_type'     => $rule->action_type,
+                    'discount_amount' => $rule->discount_amount,
+                    'expired_at'      => $existing->expired_at,
+                ], 'You have already claimed this coupon');
+            }
+
+            $code = $this->generateUniqueCode();
+            $coupon = $this->createCoupon([
+                'cart_rule_id'       => $rule->id,
+                'code'               => $code,
+                'usage_limit'        => $rule->uses_per_coupon ?: 1,
+                'usage_per_customer' => $rule->usage_per_customer ?: 1,
+                'customer_id'        => $customer->id,
+                'is_primary'         => 0,
+                'expired_at'         => $rule->ends_till ?: null,
+            ]);
+
+            return ApiResponse::success([
+                'id'              => $coupon->id,
+                'code'            => $coupon->code,
+                'rule_name'       => $rule->name,
+                'action_type'     => $rule->action_type,
+                'discount_amount' => $rule->discount_amount,
+                'expired_at'      => $coupon->expired_at,
+            ], 'Coupon claimed successfully');
+        } catch (\Exception $e) {
+            return ApiResponse::error($e->getMessage());
+        }
+    }
+
+    protected function generateUniqueCode(): string
+    {
+        $retries = 5;
+        do {
+            $code = '';
+            for ($i = 0; $i < $this->codeLength; $i++) {
+                $code .= $this->charset[random_int(0, strlen($this->charset) - 1)];
+            }
+        } while (DB::table('cart_rule_coupons')->where('code', $code)->exists() && --$retries > 0);
+
+        return $code;
+    }
+
+    protected function createCoupon(array $data)
+    {
+        $retries = 3;
+        while ($retries > 0) {
+            try {
+                return $this->cartRuleCouponRepository->create($data);
+            } catch (\Illuminate\Database\QueryException $e) {
+                if ($e->getCode() == 23000 && $retries > 1) {
+                    $retries--;
+                    $data['code'] = $this->generateUniqueCode();
+                    continue;
+                }
+                throw $e;
+            }
+        }
+        throw new \RuntimeException('Failed to generate unique coupon code');
+    }
+}

+ 271 - 0
packages/Webkul/Shop/src/Http/Controllers/API/OrderController.php

@@ -0,0 +1,271 @@
+<?php
+
+namespace Webkul\Shop\Http\Controllers\API;
+
+use Carbon\Carbon;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Longyi\RewardPoints\Helpers\ApiResponse;
+use Webkul\Sales\Models\Order;
+use Webkul\Sales\Repositories\OrderRepository;
+
+class OrderController extends APIController
+{
+    /**
+     * Create a new controller instance.
+     */
+    public function __construct(protected OrderRepository $orderRepository) {}
+
+    /**
+     * 获取当前客户订单列表(分页)
+     *
+     * @param  Request  $request
+     * @return JsonResponse
+     */
+    public function index(Request $request): JsonResponse
+    {
+        $customer = auth()->guard('customer')->user();
+        if (! $customer) {
+            return ApiResponse::unauthorized();
+        }
+
+        $page  = (int) $request->input('page', 1);
+        $limit = (int) $request->input('limit', 10);
+        $limit = min(max($limit, 1), 100);
+
+        $status = $request->input('status');
+
+        $query = Order::query()
+            ->where('customer_id', $customer->id)
+            ->orderBy('created_at', 'desc');
+
+        if ($status) {
+            $query->where('status', $status);
+        }
+
+        $paginator = $query->paginate($limit, ['*'], 'page', $page);
+
+        $orders = $paginator->through(function ($order) {
+            return $this->formatOrderListItem($order);
+        });
+
+        return ApiResponse::paginated($orders->values(), $paginator);
+    }
+
+    /**
+     * 获取订单详情
+     *
+     * @param  int  $id
+     * @return JsonResponse
+     */
+    public function show(int $id): JsonResponse
+    {
+        $customer = auth()->guard('customer')->user();
+        if (! $customer) {
+            return ApiResponse::unauthorized();
+        }
+
+        $order = Order::query()
+            ->with([
+                'items' => function ($q) {
+                    $q->whereNull('parent_id');
+                },
+                'items.child',
+                'items.additional',
+                'billing_address',
+                'shipping_address',
+                'payment',
+                'invoices',
+                'invoices.items',
+                'shipments',
+                'shipments.items',
+                'refunds',
+                'comments',
+            ])
+            ->where('customer_id', $customer->id)
+            ->where('id', $id)
+            ->first();
+
+        if (! $order) {
+            return ApiResponse::notFound('Order not found');
+        }
+
+        return ApiResponse::success($this->formatOrderDetail($order));
+    }
+
+    /**
+     * 格式化订单列表项
+     */
+    protected function formatOrderListItem(Order $order): array
+    {
+        return [
+            'id'                  => $order->id,
+            'increment_id'        => $order->increment_id,
+            'status'              => $order->status,
+            'status_label'        => $this->getStatusLabel($order->status),
+            'grand_total'         => (float) $order->grand_total,
+            'order_currency_code' => $order->order_currency_code,
+            'item_count'          => $order->items->sum('qty_ordered'),
+            'items'               => $order->items->map(fn ($item) => [
+                'id'          => $item->id,
+                'name'        => $item->name,
+                'sku'         => $item->sku,
+                'qty_ordered' => $item->qty_ordered,
+                'price'       => (float) $item->price,
+                'total'       => (float) $item->total,
+                'product_id'  => $item->product_id,
+            ])->values(),
+            'created_at'          => $order->created_at ? Carbon::parse($order->created_at)->toDateTimeString() : null,
+            'can_cancel'          => $order->canCancel(),
+            'can_reorder'         => $order->canReorder(),
+        ];
+    }
+
+    /**
+     * 格式化订单详情
+     */
+    protected function formatOrderDetail(Order $order): array
+    {
+        return [
+            'id'                  => $order->id,
+            'increment_id'        => $order->increment_id,
+            'status'              => $order->status,
+            'status_label'        => $this->getStatusLabel($order->status),
+            'customer_email'      => $order->customer_email,
+            'customer_first_name' => $order->customer_first_name,
+            'customer_last_name'  => $order->customer_last_name,
+            'channel_name'        => $order->channel_name,
+            'order_currency_code' => $order->order_currency_code,
+
+            'sub_total'        => (float) $order->sub_total,
+            'shipping_amount'  => (float) $order->shipping_amount,
+            'tax_amount'       => (float) ($order->tax_amount ?? 0),
+            'discount_amount'  => (float) ($order->discount_amount ?? 0),
+            'grand_total'      => (float) $order->grand_total,
+            'total_qty_ordered'=> $order->total_qty_ordered,
+
+            'shipping_method'  => $order->shipping_method,
+            'shipping_title'   => $order->shipping_title,
+
+            'billing_address'  => $this->formatAddress($order->billing_address),
+            'shipping_address' => $this->formatAddress($order->shipping_address),
+
+            'payment'          => $order->payment ? [
+                'method'       => $order->payment->method,
+                'method_title' => $order->payment->method_title,
+            ] : null,
+
+            'items'            => $order->items->map(fn ($item) => [
+                'id'           => $item->id,
+                'name'         => $item->name,
+                'sku'          => $item->sku,
+                'type'         => $item->type,
+                'qty_ordered'  => $item->qty_ordered,
+                'qty_shipped'  => (int) ($item->qty_shipped ?? 0),
+                'qty_invoiced' => (int) ($item->qty_invoiced ?? 0),
+                'qty_canceled' => (int) ($item->qty_canceled ?? 0),
+                'qty_refunded' => (int) ($item->qty_refunded ?? 0),
+                'price'        => (float) $item->price,
+                'total'        => (float) $item->total,
+                'weight'       => (float) ($item->weight ?? 0),
+                'product_id'   => $item->product_id,
+                'additional'   => $item->additional ? $item->additional->toArray() : null,
+            ])->values(),
+
+            'invoices'         => $order->invoices->map(fn ($invoice) => [
+                'id'           => $invoice->id,
+                'increment_id' => $invoice->increment_id ?? ('#' . $invoice->id),
+                'state'        => $invoice->state,
+                'email_sent'   => $invoice->email_sent,
+                'total_qty'    => $invoice->total_qty,
+                'sub_total'    => (float) $invoice->sub_total,
+                'grand_total'  => (float) $invoice->grand_total,
+                'created_at'   => $invoice->created_at ? Carbon::parse($invoice->created_at)->toDateTimeString() : null,
+                'items'        => $invoice->items->map(fn ($invItem) => [
+                    'name'  => $invItem->name,
+                    'qty'   => $invItem->qty,
+                    'price' => (float) $invItem->price,
+                    'total' => (float) $invItem->total,
+                ])->values(),
+            ])->values(),
+
+            'shipments'        => $order->shipments->map(fn ($shipment) => [
+                'id'           => $shipment->id,
+                'increment_id' => $shipment->increment_id ?? ('#' . $shipment->id),
+                'status'       => $shipment->status,
+                'total_qty'    => $shipment->total_qty,
+                'total_weight' => (float) $shipment->total_weight,
+                'carrier_code' => $shipment->carrier_code,
+                'carrier_title'=> $shipment->carrier_title,
+                'track_number' => $shipment->track_number,
+                'created_at'   => $shipment->created_at ? Carbon::parse($shipment->created_at)->toDateTimeString() : null,
+                'items'        => $shipment->items->map(fn ($shipItem) => [
+                    'name'     => $shipItem->name,
+                    'qty'      => $shipItem->qty,
+                ])->values(),
+            ])->values(),
+
+            'refunds'          => $order->refunds->map(fn ($refund) => [
+                'id'           => $refund->id,
+                'increment_id' => $refund->increment_id ?? ('#' . $refund->id),
+                'state'        => $refund->state,
+                'total_qty'    => $refund->total_qty,
+                'grand_total'  => (float) $refund->grand_total,
+                'created_at'   => $refund->created_at ? Carbon::parse($refund->created_at)->toDateTimeString() : null,
+            ])->values(),
+
+            'comments'         => $order->comments->map(fn ($comment) => [
+                'comment'           => $comment->comment,
+                'customer_notified' => $comment->customer_notified,
+                'created_at'        => $comment->created_at ? Carbon::parse($comment->created_at)->toDateTimeString() : null,
+            ])->values(),
+
+            'can_cancel'       => $order->canCancel(),
+            'can_reorder'      => $order->canReorder(),
+
+            'created_at'       => $order->created_at ? Carbon::parse($order->created_at)->toDateTimeString() : null,
+            'updated_at'       => $order->updated_at ? Carbon::parse($order->updated_at)->toDateTimeString() : null,
+        ];
+    }
+
+    /**
+     * 格式化地址
+     */
+    protected function formatAddress($address): ?array
+    {
+        if (! $address) {
+            return null;
+        }
+
+        return [
+            'first_name' => $address->first_name,
+            'last_name'  => $address->last_name,
+            'email'      => $address->email,
+            'phone'      => $address->phone,
+            'address1'   => $address->address1,
+            'address2'   => $address->address2,
+            'city'       => $address->city,
+            'state'      => $address->state,
+            'country'    => $address->country,
+            'postcode'   => $address->postcode,
+            'company'    => $address->company_name,
+        ];
+    }
+
+    /**
+     * 获取状态中文标签
+     */
+    protected function getStatusLabel(string $status): string
+    {
+        return match ($status) {
+            Order::STATUS_PENDING         => '待处理',
+            Order::STATUS_PENDING_PAYMENT => '待付款',
+            Order::STATUS_PROCESSING      => '处理中',
+            Order::STATUS_COMPLETED       => '已完成',
+            Order::STATUS_CANCELED        => '已取消',
+            Order::STATUS_CLOSED          => '已关闭',
+            Order::STATUS_FRAUD           => '欺诈',
+            default                       => $status,
+        };
+    }
+}

+ 13 - 0
packages/Webkul/Shop/src/Resources/lang/en/app.php

@@ -356,6 +356,19 @@ return [
                 'title'               => 'Title',
             ],
 
+            'coupons' => [
+                'already-applied'      => 'This coupon is already applied to your cart.',
+                'cannot-apply'         => 'Could not apply coupon, please check cart conditions.',
+                'claimed'              => 'Coupon claimed and applied successfully.',
+                'expired'              => 'This coupon has expired.',
+                'no-coupons'           => 'No coupons available.',
+                'not-found'            => 'Coupon code not found.',
+                'not-yours'            => 'This coupon does not belong to you.',
+                'page-title'           => 'My Coupons',
+                'rule-inactive'        => 'This coupon rule is currently inactive.',
+                'usage-limit-reached'  => 'This coupon has reached its usage limit.',
+            ],
+
             'wishlist' => [
                 'color'              => 'Color',
                 'delete-all'         => 'Delete All',

+ 13 - 0
packages/Webkul/Shop/src/Resources/lang/zh_CN/app.php

@@ -356,6 +356,19 @@ return [
                 'title'               => '名称',
             ],
 
+            'coupons' => [
+                'already-applied'      => '此优惠券已应用于您的购物车。',
+                'cannot-apply'         => '无法使用该优惠券,请检查购物车条件。',
+                'claimed'              => '优惠券领取成功并已应用到购物车。',
+                'expired'              => '此优惠券已过期。',
+                'no-coupons'           => '暂无可用的优惠券。',
+                'not-found'            => '未找到此优惠券。',
+                'not-yours'            => '此优惠券不属于您。',
+                'page-title'           => '我的优惠券',
+                'rule-inactive'        => '该优惠券规则当前未激活。',
+                'usage-limit-reached'  => '此优惠券已达到使用次数上限。',
+            ],
+
             'wishlist' => [
                 'color'              => '颜色',
                 'delete-all'         => '全部删除',

+ 20 - 0
packages/Webkul/Shop/src/Routes/api-token.php

@@ -2,6 +2,8 @@
 
 use Illuminate\Support\Facades\Route;
 use Webkul\Shop\Http\Controllers\API\BearerAddressController;
+use Webkul\Shop\Http\Controllers\API\CouponController;
+use Webkul\Shop\Http\Controllers\API\OrderController;
 
 /**
  * Bearer Token 认证的 API 路由
@@ -24,4 +26,22 @@ Route::group(['prefix' => 'api'], function () {
             Route::delete('{id}', 'destroy')->name('shop.api.token.customer.addresses.destroy');
         });
 
+    /** 专属优惠券(Bearer Token 认证) */
+    Route::controller(CouponController::class)
+        ->middleware('bearer.token')
+        ->prefix('customer/coupons')
+        ->group(function () {
+            Route::get('', 'index')->name('shop.api.token.customer.coupons.index');
+            Route::post('claim', 'claim')->name('shop.api.token.customer.coupons.claim');
+        });
+
+    /** 客户订单管理(Bearer Token 认证) */
+    Route::controller(OrderController::class)
+        ->middleware('bearer.token')
+        ->prefix('customer/orders')
+        ->group(function () {
+            Route::get('', 'index')->name('shop.api.token.customer.orders.index');
+            Route::get('{id}', 'show')->name('shop.api.token.customer.orders.show');
+        });
+
 });