Ver Fonte

Merge branch 'llp_member_26_04_16' into dev

llp há 20 horas atrás
pai
commit
3334667ad4
52 ficheiros alterados com 1412 adições e 15 exclusões
  1. 1 0
      bootstrap/providers.php
  2. 1 0
      composer.json
  3. 8 8
      packages/Longyi/Gift/src/Listeners/InvoiceHandler.php
  4. 1 1
      packages/Longyi/Gift/src/Providers/EventServiceProvider.php
  5. 12 3
      packages/Longyi/Gift/src/Repositories/CustomInvoiceRepository.php
  6. 10 2
      packages/Longyi/Gift/src/Repositories/CustomOrderRepository.php
  7. 21 0
      packages/Longyi/Gift/src/Resources/views/sales/invoices/view.blade.php
  8. 4 1
      packages/Longyi/Gift/src/Resources/views/sales/orders/view.blade.php
  9. 23 0
      packages/Longyi/Member/package.json
  10. 6 0
      packages/Longyi/Member/postcss.config.js
  11. 10 0
      packages/Longyi/Member/src/Config/acl.php
  12. 11 0
      packages/Longyi/Member/src/Config/admin-menu.php
  13. 7 0
      packages/Longyi/Member/src/Contracts/MemberLog.php
  14. 107 0
      packages/Longyi/Member/src/DataGrids/Member/MemberDataGrid.php
  15. 28 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_233520_add_vip_expire_fields_to_customer_table.php
  16. 31 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_233955_add_member_fields_to_cart_table.php
  17. 31 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_234016_add_member_fields_to_orders_table.php
  18. 31 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_15_234035_add_member_fields_to_invoices_table.php
  19. 32 0
      packages/Longyi/Member/src/Database/Migrations/2026_04_20_180518_create_member_log_table.php
  20. 23 0
      packages/Longyi/Member/src/Http/Controllers/Admin/MemberController.php
  21. 17 0
      packages/Longyi/Member/src/Http/Controllers/Shop/MemberController.php
  22. 99 0
      packages/Longyi/Member/src/Http/Controllers/Shop/MemberDiscountController.php
  23. 47 0
      packages/Longyi/Member/src/Listeners/MemberHandler.php
  24. 63 0
      packages/Longyi/Member/src/Listeners/OrderPlacedHandler.php
  25. 28 0
      packages/Longyi/Member/src/Listeners/OrderViewHandler.php
  26. 98 0
      packages/Longyi/Member/src/Listeners/VipDiscountHandler.php
  27. 39 0
      packages/Longyi/Member/src/Models/MemberLog.php
  28. 9 0
      packages/Longyi/Member/src/Models/MemberLogProxy.php
  29. 33 0
      packages/Longyi/Member/src/Providers/EventServiceProvider.php
  30. 55 0
      packages/Longyi/Member/src/Providers/MemberServiceProvider.php
  31. 15 0
      packages/Longyi/Member/src/Providers/ModuleServiceProvider.php
  32. 16 0
      packages/Longyi/Member/src/Repositories/MemberLogRepository.php
  33. 23 0
      packages/Longyi/Member/src/Resources/assets/css/app.css
  34. 12 0
      packages/Longyi/Member/src/Resources/assets/images/icon-temp-active.svg
  35. 12 0
      packages/Longyi/Member/src/Resources/assets/images/icon-temp.svg
  36. 4 0
      packages/Longyi/Member/src/Resources/assets/js/app.js
  37. 9 0
      packages/Longyi/Member/src/Resources/lang/en/app.php
  38. 9 0
      packages/Longyi/Member/src/Resources/lang/zh_CN/app.php
  39. 19 0
      packages/Longyi/Member/src/Resources/views/admin/index.blade.php
  40. 1 0
      packages/Longyi/Member/src/Resources/views/admin/layouts/style.blade.php
  41. 16 0
      packages/Longyi/Member/src/Resources/views/components/member-discount-cartsummary.blade.php
  42. 183 0
      packages/Longyi/Member/src/Resources/views/components/vip-member-discount-cartsummary.blade.php
  43. 22 0
      packages/Longyi/Member/src/Resources/views/customers/account/orders/member-info.blade.php
  44. 29 0
      packages/Longyi/Member/src/Resources/views/sales/orders/view.blade.php
  45. 11 0
      packages/Longyi/Member/src/Resources/views/shop/index.blade.php
  46. 10 0
      packages/Longyi/Member/src/Routes/admin-routes.php
  47. 14 0
      packages/Longyi/Member/src/Routes/shop-routes.php
  48. 4 0
      packages/Longyi/Member/tailwind.config.js
  49. 46 0
      packages/Longyi/Member/vite.config.js
  50. 11 0
      packages/Webkul/Customer/src/Models/Customer.php
  51. 9 0
      packages/Webkul/Sales/src/Transformers/OrderResource.php
  52. 51 0
      packages/Webkul/Shop/src/Http/Resources/CartResource.php

+ 1 - 0
bootstrap/providers.php

@@ -13,6 +13,7 @@ return [
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
     Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
     Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
+    Longyi\Member\Providers\MemberServiceProvider::class,
     Longyi\Gift\Providers\GiftServiceProvider::class,
     Webkul\Attribute\Providers\AttributeServiceProvider::class,
     Webkul\BookingProduct\Providers\BookingProductServiceProvider::class,

+ 1 - 0
composer.json

@@ -70,6 +70,7 @@
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
             "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
             "Longyi\\RewardPoints\\": "packages/Longyi/RewardPoints/src/",
+            "Longyi\\Member\\": "packages/Longyi/Member/src/",
             "Longyi\\Gift\\": "packages/Longyi/Gift/src/",
             "Webkul\\Admin\\": "packages/Webkul/Admin/src",
             "Webkul\\Attribute\\": "packages/Webkul/Attribute/src",

+ 8 - 8
packages/Longyi/Gift/src/Listeners/InvoiceHandler.php

@@ -21,13 +21,13 @@ class InvoiceHandler
             return;
         }
 
-        // 更新发票的礼品卡信息
-        DB::table('invoices')
-            ->where('id', $invoice->id)
-            ->update([
-                'giftcard_number' => $order->giftcard_number,
-                'giftcard_amount' => $order->giftcard_amount,
-                'base_giftcard_amount' => $order->base_giftcard_amount,
-            ]);
+//        // 更新发票的礼品卡信息
+//        DB::table('invoices')
+//            ->where('id', $invoice->id)
+//            ->update([
+//                'giftcard_number' => $order->giftcard_number,
+//                'giftcard_amount' => $order->giftcard_amount,
+//                'base_giftcard_amount' => $order->base_giftcard_amount,
+//            ]);
     }
 }

+ 1 - 1
packages/Longyi/Gift/src/Providers/EventServiceProvider.php

@@ -16,7 +16,7 @@ class EventServiceProvider extends ServiceProvider
     {
         Event::listen('checkout.cart.collect.totals.after', 'Longyi\Gift\Listeners\GiftHandler@applyGiftCard');
         Event::listen('checkout.order.save.after', 'Longyi\Gift\Listeners\OrderPlacedHandler@afterPlaceOrder');
-        Event::listen('bagisto.shop.checkout.onepage.summary.coupon.after', function($viewRenderEventManager) {
+        Event::listen('bagisto.shop.checkout.cart.summary.member_discount.after', function($viewRenderEventManager) {
             $viewRenderEventManager->addTemplate('gift::components.giftcard-cartsummary');
         });
         Event::listen('bagisto.shop.customers.account.orders.view.information.discount.after', 'Longyi\Gift\Listeners\OrderViewHandler@renderGiftCardInfo');

+ 12 - 3
packages/Longyi/Gift/src/Repositories/CustomInvoiceRepository.php

@@ -2,6 +2,7 @@
 
 namespace Longyi\Gift\Repositories;
 
+use Illuminate\Support\Facades\Event;
 use Webkul\Sales\Repositories\InvoiceRepository as BaseInvoiceRepository;
 
 class CustomInvoiceRepository extends BaseInvoiceRepository
@@ -13,6 +14,10 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
         $invoice->tax_amount = $invoice->base_tax_amount = 0;
         $invoice->shipping_tax_amount = $invoice->shipping_tax_amount = 0;
         $invoice->discount_amount = $invoice->base_discount_amount = 0;
+        $invoice->giftcard_amount = $invoice->giftcard_amount = 0;
+        $invoice->base_giftcard_amount = $invoice->base_giftcard_amount = 0;
+        $invoice->vip_plus_amount = $invoice->base_vip_plus_amount = 0;
+        $invoice->vip_discount_amount = $invoice->base_vip_discount_amount = 0;
 
         foreach ($invoice->items as $item) {
             $invoice->tax_amount += $item->tax_amount;
@@ -41,6 +46,11 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
         $invoice->base_giftcard_amount += $invoice->order->base_giftcard_amount;
         $invoice->giftcard_number = $invoice->order->giftcard_number;
 
+        $invoice->vip_plus_amount += $invoice->order->vip_plus_amount;
+        $invoice->base_vip_plus_amount += $invoice->order->base_vip_plus_amount;
+        $invoice->vip_discount_amount += $invoice->order->vip_discount_amount;
+        $invoice->base_vip_discount_amount += $invoice->order->base_vip_discount_amount;
+
         if ($invoice->order->shipping_tax_amount) {
             $invoice->shipping_tax_amount = $invoice->order->shipping_tax_amount;
 
@@ -75,11 +85,10 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
             }
         }
 
-        $invoice->grand_total = $invoice->sub_total + $invoice->tax_amount + $invoice->shipping_amount - $invoice->discount_amount - $invoice->giftcard_amount;
-        $invoice->base_grand_total = $invoice->base_sub_total + $invoice->base_tax_amount + $invoice->base_shipping_amount - $invoice->base_discount_amount - $invoice->base_giftcard_amount;
+        $invoice->grand_total = $invoice->sub_total + $invoice->tax_amount + $invoice->shipping_amount - $invoice->discount_amount - $invoice->giftcard_amount + $invoice->vip_plus_amount - $invoice->vip_discount_amount;
+        $invoice->base_grand_total = $invoice->base_sub_total + $invoice->base_tax_amount + $invoice->base_shipping_amount - $invoice->base_discount_amount - $invoice->base_giftcard_amount + $invoice->base_vip_plus_amount - $invoice->base_vip_discount_amount;
 
         $invoice->save();
-
         return $invoice;
     }
 }

+ 10 - 2
packages/Longyi/Gift/src/Repositories/CustomOrderRepository.php

@@ -14,6 +14,8 @@ class CustomOrderRepository extends BaseOrderRepository
         $order->tax_amount_invoiced = $order->base_tax_amount_invoiced = 0;
         $order->discount_invoiced = $order->base_discount_invoiced = 0;
         $order->giftcard_amount = $order->base_giftcard_amount = 0;
+        $order->vip_plus_amount = $order->base_vip_plus_amount = 0;
+        $order->vip_discount_amount = $order->base_vip_discount_amount = 0;
 
         foreach ($order->invoices as $invoice) {
             $order->sub_total_invoiced += $invoice->sub_total;
@@ -30,10 +32,16 @@ class CustomOrderRepository extends BaseOrderRepository
 
             $order->giftcard_amount += $invoice->giftcard_amount;
             $order->base_giftcard_amount += $invoice->base_giftcard_amount;
+
+            $order->vip_plus_amount += $invoice->vip_plus_amount;
+            $order->base_vip_plus_amount += $invoice->base_vip_plus_amount;
+
+            $order->vip_discount_amount += $invoice->vip_discount_amount;
+            $order->base_vip_discount_amount += $invoice->base_vip_discount_amount;
         }
 
-        $order->grand_total_invoiced = $order->sub_total_invoiced + $order->shipping_invoiced + $order->tax_amount_invoiced - $order->discount_invoiced - $order->giftcard_amount;
-        $order->base_grand_total_invoiced = $order->base_sub_total_invoiced + $order->base_shipping_invoiced + $order->base_tax_amount_invoiced - $order->base_discount_invoiced - $order->base_giftcard_amount;
+        $order->grand_total_invoiced = $order->sub_total_invoiced + $order->shipping_invoiced + $order->tax_amount_invoiced - $order->discount_invoiced - $order->giftcard_amount + $order->vip_plus_amount - $order->vip_discount_amount;
+        $order->base_grand_total_invoiced = $order->base_sub_total_invoiced + $order->base_shipping_invoiced + $order->base_tax_amount_invoiced - $order->base_discount_invoiced - $order->base_giftcard_amount + $order->base_vip_plus_amount - $order->base_vip_discount_amount;
 
         // order refund total
         $order->sub_total_refunded = $order->base_sub_total_refunded = 0;

+ 21 - 0
packages/Longyi/Gift/src/Resources/views/sales/invoices/view.blade.php

@@ -297,6 +297,7 @@
                                 @lang('admin::app.sales.invoices.view.summary-discount')
                             </p>
                         @endif
+
                         @if ($order->giftcard_amount > 0)
                             <p class="text-gray-600 dark:text-gray-300 !leading-5 text-sm">
                                 @lang('gift::app.giftcard.giftcard_amount')
@@ -308,6 +309,16 @@
                                 @lang('gift::app.giftcard.giftcard_number')
                             </p>
                         @endif
+                        @if ($order->base_vip_plus_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5 text-sm">
+                                @lang('member::app.member.discount')
+                            </p>
+                        @endif
+                        @if ($order->base_vip_discount_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5 text-sm">
+                                @lang('member::app.member.vip_discount')
+                            </p>
+                        @endif
                         <p class="text-base font-semibold !leading-5 text-gray-800 dark:text-white">
                             @lang('admin::app.sales.invoices.view.grand-total')
                         </p>
@@ -377,6 +388,16 @@
                                 {{ ($invoice->giftcard_number) }}
                             </p>
                         @endif
+                        @if ($order->base_vip_plus_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5">
+                                +  {{ core()->formatBasePrice($invoice->base_vip_plus_amount) }}
+                            </p>
+                        @endif
+                        @if ($order->base_vip_discount_amount > 0)
+                            <p class="text-gray-600 dark:text-gray-300 !leading-5">
+                                -  {{ core()->formatBasePrice($invoice->base_vip_discount_amount) }}
+                            </p>
+                        @endif
                         <!-- Grand Total -->
                         <p class="text-base font-semibold !leading-5 text-gray-800 dark:text-white">
                             {{ core()->formatBasePrice($invoice->base_grand_total) }}

+ 4 - 1
packages/Longyi/Gift/src/Resources/views/sales/orders/view.blade.php

@@ -390,6 +390,9 @@
 
                             {!! view_render_event('bagisto.admin.sales.order.view.discount.before') !!}
 
+                            {!! view_render_event('bagisto.admin.sales.order.view.discount.after') !!}
+
+                            {!! view_render_event('bagisto.admin.sales.order.view.giftcard.before', ['order' => $order]) !!}
                             <!-- Discount -->
                             <div class="flex justify-between w-full gap-x-5">
                                 <p class="!leading-5 text-gray-600 dark:text-gray-300">
@@ -416,7 +419,7 @@
                                 </div>
                             @endif
 
-                            {!! view_render_event('bagisto.admin.sales.order.view.discount.after') !!}
+                            {!! view_render_event('bagisto.admin.sales.order.view.giftcard.after') !!}
 
                             {!! view_render_event('bagisto.admin.sales.order.view.grand-total.before') !!}
 

+ 23 - 0
packages/Longyi/Member/package.json

@@ -0,0 +1,23 @@
+{
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build"
+  },
+  "devDependencies": {
+    "autoprefixer": "^10.4.14",
+    "axios": "^1.4.0",
+    "laravel-vite-plugin": "^0.7.2",
+    "postcss": "^8.4.23",
+    "tailwindcss": "^3.3.2",
+    "vite": "^4.0.0",
+    "vue": "^3.2.47"
+  },
+  "dependencies": {
+    "@vee-validate/i18n": "^4.9.1",
+    "@vee-validate/rules": "^4.9.1",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "mitt": "^3.0.1",
+    "vee-validate": "^4.9.1"
+  }
+}

+ 6 - 0
packages/Longyi/Member/postcss.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}

+ 10 - 0
packages/Longyi/Member/src/Config/acl.php

@@ -0,0 +1,10 @@
+<?php
+
+return [
+    [
+        'key'   => 'member',
+        'name'  => 'Member',
+        'route' => 'admin.member.index',
+        'sort'  => 101
+    ]
+];

+ 11 - 0
packages/Longyi/Member/src/Config/admin-menu.php

@@ -0,0 +1,11 @@
+<?php
+
+return [
+    [
+        'key'   => 'member',
+        'name'  => 'Member',
+        'route' => 'admin.member.index',
+        'sort'  => 101,
+        'icon'  => 'icon-sales',
+    ]
+];

+ 7 - 0
packages/Longyi/Member/src/Contracts/MemberLog.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Longyi\Member\Contracts;
+
+interface MemberLog
+{
+}

+ 107 - 0
packages/Longyi/Member/src/DataGrids/Member/MemberDataGrid.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Longyi\Member\DataGrids\Member;
+
+use Illuminate\Support\Facades\DB;
+use Webkul\DataGrid\DataGrid;
+
+class MemberDataGrid extends DataGrid
+{
+    /**
+     * Prepare query builder.
+     *
+     * @return \Illuminate\Database\Query\Builder
+     */
+    public function prepareQueryBuilder()
+    {
+        $queryBuilder = DB::table('member_log')
+            ->leftJoin('customers', 'member_log.customer_id', '=', 'customers.id')
+            ->select(
+                'member_log.id',
+                'member_log.customer_id',
+                'member_log.order_id',
+                'member_log.amount',
+                'customers.email as customer_email',
+                'customers.first_name',
+                'customers.last_name',
+                'member_log.expirationdate',
+                'member_log.created_at',
+                'member_log.updated_at'
+            );
+
+        $this->addFilter('id', 'member_log.id');
+        $this->addFilter('order_id', 'member_log.order_id');
+        $this->addFilter('amount', 'member_log.amount');
+        $this->addFilter('expirationdate', 'member_log.expirationdate');
+        $this->addFilter('created_at', 'member_log.created_at');
+        $this->addFilter('customer_email', 'customers.email');
+
+        return $queryBuilder;
+    }
+
+    /**
+     * Add columns.
+     *
+     * @return void
+     */
+    public function prepareColumns()
+    {
+        $this->addColumn([
+            'index'      => 'id',
+            'label'      => 'id',
+            'type'       => 'integer',
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+        $this->addColumn([
+            'index'      => 'customer_email',
+            'label'      => '用户邮箱',
+            'type'       => 'string',
+            'filterable' => true,
+            'sortable'   => true,
+            'searchable' => true,
+            'closure'    => function ($row) {
+                if ($row->customer_email) {
+                    $customerName = '';
+                    if ($row->first_name || $row->last_name) {
+                        $customerName = ' (' . trim($row->first_name . ' ' . $row->last_name) . ')';
+                    }
+                    return $row->customer_email . $customerName;
+                }
+            },
+        ]);
+        $this->addColumn([
+            'index'      => 'order_id',
+            'label'      => '#order Id',
+            'type'       => 'integer',
+            'filterable' => false,
+            'sortable'   => true
+        ]);
+        $this->addColumn([
+            'index'      => 'amount',
+            'label'      => 'vip amount',
+            'type'       => 'aggregate',
+            'filterable' => false,
+            'sortable'   => true,
+            'closure'    => function ($row) {
+                return core()->formatPrice($row->amount);
+            },
+        ]);
+        $this->addColumn([
+            'index'      => 'expirationdate',
+            'label'      => 'expirationdate',
+            'type'       => 'date',
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+
+
+        $this->addColumn([
+            'index'      => 'created_at',
+            'label'      => 'created at',
+            'type'       => 'datetime',
+            'filterable' => true,
+            'sortable'   => true,
+        ]);
+    }
+}

+ 28 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_233520_add_vip_expire_fields_to_customer_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('customers', function (Blueprint $table) {
+            $table->dateTime('vip_expire_date')->nullable()->after('remember_token');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('customers', function (Blueprint $table) {
+            $table->dropColumn('vip_expire_date');
+        });
+    }
+};

+ 31 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_233955_add_member_fields_to_cart_table.php

@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            $table->decimal('vip_plus_amount', 10, 2)->nullable()->after('base_giftcard_amount');
+            $table->decimal('base_vip_plus_amount', 10, 2)->nullable()->after('vip_plus_amount');
+            $table->decimal('vip_discount_amount', 10, 2)->nullable()->after('base_vip_plus_amount');
+            $table->decimal('base_vip_discount_amount', 10, 2)->nullable()->after('vip_discount_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            $table->dropColumn(['vip_plus_amount','base_vip_plus_amount', 'vip_discount_amount', 'vip_discount_amount']);
+        });
+    }
+};

+ 31 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_234016_add_member_fields_to_orders_table.php

@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->decimal('vip_plus_amount', 10, 2)->nullable()->after('base_giftcard_amount');
+            $table->decimal('base_vip_plus_amount', 10, 2)->nullable()->after('vip_plus_amount');
+            $table->decimal('vip_discount_amount', 10, 2)->nullable()->after('base_vip_plus_amount');
+            $table->decimal('base_vip_discount_amount', 10, 2)->nullable()->after('vip_discount_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->dropColumn(['vip_plus_amount','base_vip_plus_amount', 'vip_discount_amount', 'vip_discount_amount']);
+        });
+    }
+};

+ 31 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_15_234035_add_member_fields_to_invoices_table.php

@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('invoices', function (Blueprint $table) {
+            $table->decimal('vip_plus_amount', 10, 2)->nullable()->after('base_giftcard_amount');
+            $table->decimal('base_vip_plus_amount', 10, 2)->nullable()->after('vip_plus_amount');
+            $table->decimal('vip_discount_amount', 10, 2)->nullable()->after('base_vip_plus_amount');
+            $table->decimal('base_vip_discount_amount', 10, 2)->nullable()->after('vip_discount_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('invoices', function (Blueprint $table) {
+            $table->dropColumn(['vip_plus_amount','base_vip_plus_amount', 'vip_discount_amount', 'vip_discount_amount']);
+        });
+    }
+};

+ 32 - 0
packages/Longyi/Member/src/Database/Migrations/2026_04_20_180518_create_member_log_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('member_log', function (Blueprint $table) {
+            $table->id();
+            $table->integer('customer_id')->index()->default(0)->nullable();
+            $table->integer('order_id')->index()->default(0)->nullable();
+            $table->tinyInteger('type')->default(1)->nullable();
+            $table->decimal('amount', 10, 2)->nullable()->comment('amount');
+            $table->dateTime('expirationdate')->nullable()->comment('过期时间');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('member_log');
+    }
+};

+ 23 - 0
packages/Longyi/Member/src/Http/Controllers/Admin/MemberController.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Longyi\Member\Http\Controllers\Admin;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\View\View;
+use Longyi\Member\DataGrids\Member\MemberDataGrid;
+use Webkul\Admin\Http\Controllers\Controller;
+
+class MemberController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index(): View|JsonResponse
+    {
+        if (request()->ajax()) {
+            return datagrid(MemberDataGrid::class)->process();
+        }
+
+        return view('member::admin.index');
+    }
+}

+ 17 - 0
packages/Longyi/Member/src/Http/Controllers/Shop/MemberController.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Longyi\Member\Http\Controllers\Shop;
+
+use Illuminate\View\View;
+use Webkul\Shop\Http\Controllers\Controller;
+
+class MemberController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index(): View
+    {
+        return view('member::shop.index');
+    }
+}

+ 99 - 0
packages/Longyi/Member/src/Http/Controllers/Shop/MemberDiscountController.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace Longyi\Member\Http\Controllers\Shop;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Response;
+use Webkul\Checkout\Facades\Cart;
+use Webkul\Shop\Http\Controllers\Controller;
+
+class MemberDiscountController extends Controller
+{
+    protected const MEMBER_DISCOUNT_AMOUNT = 19.9;
+
+    /**
+     * Apply member discount to cart.
+     */
+    public function apply(): JsonResponse
+    {
+        try {
+            if (!auth()->guard('customer')->check()) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Please log in to your member account first',
+                ], Response::HTTP_UNAUTHORIZED);
+            }
+
+            $cart = Cart::getCart();
+
+            if (!$cart) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Shopping cart is empty',
+                ], Response::HTTP_NOT_FOUND);
+            }
+
+            if ($cart->vip_plus_amount && $cart->vip_plus_amount > 0) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Member discount has been applied',
+                ], Response::HTTP_OK);
+            }
+
+            $discountAmountInCurrentCurrency = core()->convertPrice(self::MEMBER_DISCOUNT_AMOUNT);
+
+            $cart->vip_plus_amount = $discountAmountInCurrentCurrency;
+            $cart->base_vip_plus_amount = core()->convertToBasePrice($discountAmountInCurrentCurrency);
+            $cart->save();
+            Cart::collectTotals();
+
+            return response()->json([
+                'success' => true,
+                'message' => 'Member discount has been applied',
+                'discount_amount' => $cart->vip_plus_amount,
+            ]);
+        } catch (\Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Failed to apply member discount',
+                'error' => $e->getMessage(),
+            ], Response::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Remove member discount from cart.
+     */
+    public function remove(): JsonResponse
+    {
+        try {
+            $cart = Cart::getCart();
+
+            if (!$cart || !$cart->vip_plus_amount || $cart->vip_plus_amount <= 0) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Member discount not applied',
+                ], Response::HTTP_OK);
+            }
+
+            $discountAmount = $cart->vip_plus_amount;
+            $baseDiscountAmount = $cart->base_vip_plus_amount;
+
+            $cart->vip_plus_amount = null;
+            $cart->base_vip_plus_amount = null;
+            $cart->save();
+            Cart::collectTotals();
+
+            return response()->json([
+                'success' => true,
+                'message' => 'Member discount has been removed',
+            ]);
+        } catch (\Exception $e) {
+            return response()->json([
+                'success' => false,
+                'message' => 'Failed to remove member discount',
+                'error' => $e->getMessage(),
+            ], Response::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+}

+ 47 - 0
packages/Longyi/Member/src/Listeners/MemberHandler.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Webkul\Paypal\Payment\SmartButton;
+use Webkul\Sales\Repositories\OrderTransactionRepository;
+
+class MemberHandler
+{
+    protected const MEMBER_DISCOUNT_AMOUNT = 19.9;
+    /**
+     * Create a new listener instance.
+     *
+     * @return void
+     */
+    public function __construct(
+        protected SmartButton $smartButton,
+        protected OrderTransactionRepository $orderTransactionRepository
+    ) {}
+
+    /**
+     * Apply member discount to cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function applyMemberDiscount($cart)
+    {
+
+        if (!$cart || !auth()->guard('customer')->check()) {
+            return;
+        }
+        if ($cart->vip_plus_amount <= 0) {
+            return;
+        }
+        $discountAmountInCurrentCurrency = core()->convertPrice(self::MEMBER_DISCOUNT_AMOUNT);
+
+        $cart->vip_plus_amount = $discountAmountInCurrentCurrency;
+        $cart->base_vip_plus_amount = core()->convertToBasePrice($discountAmountInCurrentCurrency);
+
+        $cart->grand_total = max(0, round($cart->grand_total + $discountAmountInCurrentCurrency, 2));
+        $cart->base_grand_total = max(0, round($cart->base_grand_total + $cart->base_vip_plus_amount, 2));
+
+        $cart->save();
+    }
+
+}

+ 63 - 0
packages/Longyi/Member/src/Listeners/OrderPlacedHandler.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Longyi\Member\Models\MemberLog;
+use Webkul\Paypal\Payment\SmartButton;
+use Webkul\Sales\Repositories\OrderTransactionRepository;
+use Webkul\Customer\Models\Customer;
+use Carbon\Carbon;
+
+class OrderPlacedHandler
+{
+    protected const MEMBER_DISCOUNT_AMOUNT = 19.9;
+
+
+    protected const VIP_EXTENSION_DAYS = 180;
+    /**
+     * Create a new listener instance.
+     *
+     * @return void
+     */
+    public function __construct(
+        protected SmartButton $smartButton,
+        protected OrderTransactionRepository $orderTransactionRepository
+    ) {}
+
+    /**
+     * Apply member discount to cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function afterPlaceOrder($order)
+    {
+        if (!$order || !$order->customer_id) {
+            return;
+        }
+        if (!$order->vip_plus_amount || $order->vip_plus_amount <= 0) {
+            return;
+        }
+        $customer = Customer::find($order->customer_id);
+        if (!$customer) {
+            return;
+        }
+        $currentExpireDate = $customer->vip_expire_date ? Carbon::parse($customer->vip_expire_date) : now();
+        if ($currentExpireDate->isPast()) {
+            $newExpireDate = now()->addDays(self::VIP_EXTENSION_DAYS);
+        } else {
+            $newExpireDate = $currentExpireDate->addDays(self::VIP_EXTENSION_DAYS);
+        }
+        $customer->vip_expire_date = $newExpireDate;
+        $customer->save();
+        // 记录
+        MemberLog::create([
+            'customer_id' => $order->customer_id,
+            'order_id' => $order->increment_id,
+            'amount' => self::MEMBER_DISCOUNT_AMOUNT,
+            'type' => 1,
+            'expirationdate' => $newExpireDate
+        ]);
+    }
+
+}

+ 28 - 0
packages/Longyi/Member/src/Listeners/OrderViewHandler.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Webkul\Sales\Models\Order;
+
+class OrderViewHandler
+{
+    /**
+     * Handle order view information discount before event.
+     *
+     * @param  array  $data
+     * @return void
+     */
+    public function renderMemberInfo($viewRenderEventManager)
+    {
+        // 从当前路由参数中获取订单ID
+        $orderId = request()->route('id');
+
+        if (!$orderId) {
+            return;
+        }
+
+        // 查询订单
+        $order = Order::find($orderId);
+        echo view('member::customers.account.orders.member-info', compact('order'))->render();
+    }
+}

+ 98 - 0
packages/Longyi/Member/src/Listeners/VipDiscountHandler.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Carbon\Carbon;
+
+class VipDiscountHandler
+{
+    protected const VIP_DISCOUNT_PERCENTAGE = 0.05; // 5%
+
+    /**
+     * Apply VIP discount to cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function applyVipDiscount($cart)
+    {
+        if (!$cart || !auth()->guard('customer')->check()) {
+            return;
+        }
+        $isVip = false;
+        $customer = auth()->guard('customer')->user();
+        if ($cart->vip_plus_amount > 0 ||  $this->isValidVip($customer)) {
+            $isVip = true;
+        }
+        // 检查是否是有效 VIP
+        if (!$isVip) {
+            // 如果不是有效 VIP,清除可能存在的 VIP 折扣
+            if ($cart->vip_discount_amount && $cart->vip_discount_amount > 0) {
+                $this->removeVipDiscount($cart);
+            }
+            return;
+        }
+
+        // 计算购物车小计
+        $subTotal = $cart->sub_total;
+
+        if ($subTotal <= 0) {
+            return;
+        }
+
+        // 计算 5% 折扣
+        $discountAmountInCurrentCurrency = round($subTotal * self::VIP_DISCOUNT_PERCENTAGE, 2);
+        $baseDiscountAmount = core()->convertToBasePrice($discountAmountInCurrentCurrency);
+        // 应用新的 VIP 折扣
+        $cart->vip_discount_amount = $discountAmountInCurrentCurrency;
+        $cart->base_vip_discount_amount = $baseDiscountAmount;
+
+        $cart->grand_total = max(0, round($cart->grand_total - $discountAmountInCurrentCurrency, 2));
+        $cart->base_grand_total = max(0, round($cart->base_grand_total - $baseDiscountAmount, 2));
+
+        $cart->save();
+    }
+
+    /**
+     * Remove VIP discount from cart.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function removeVipDiscount($cart)
+    {
+        if (!$cart || !$cart->vip_discount_amount || $cart->vip_discount_amount <= 0) {
+            return;
+        }
+
+        // 1. 先取出要恢复的金额(在清空之前!)
+        $discountAmount = $cart->vip_discount_amount;
+        $baseDiscountAmount = $cart->base_vip_discount_amount;
+
+        // 2. 再清空字段
+        $cart->vip_discount_amount = null;
+        $cart->base_vip_discount_amount = null;
+
+        // 3. 恢复购物车总额
+        $cart->grand_total = round($cart->grand_total, 2);
+        $cart->base_grand_total = round($cart->base_grand_total, 2);
+
+        $cart->save();
+    }
+
+    /**
+     * Check if customer is a valid VIP.
+     *
+     * @param  \Webkul\Customer\Models\Customer  $customer
+     * @return bool
+     */
+    protected function isValidVip($customer): bool
+    {
+        if (!$customer->vip_expire_date) {
+            return false;
+        }
+
+        $expireDate = Carbon::parse($customer->vip_expire_date);
+        return !$expireDate->isPast();
+    }
+}

+ 39 - 0
packages/Longyi/Member/src/Models/MemberLog.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Longyi\Member\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Longyi\Member\Contracts\MemberLog as MemberLogContract;
+
+class MemberLog extends Model implements MemberLogContract
+{
+    use HasFactory;
+
+    protected $table = 'member_log';
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'customer_id',
+        'order_id',
+        'type',
+        'amount',
+        'expirationdate'
+    ];
+
+    /**
+     * The attributes that should be cast.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'customer_id' => 'integer',
+        'order_id' => 'integer',
+        'type' => 'integer',
+        'amount' => 'decimal:2',
+        'expirationdate' => 'date',
+    ];
+}

+ 9 - 0
packages/Longyi/Member/src/Models/MemberLogProxy.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Longyi\Member\Models;
+
+use Konekt\Concord\Proxies\ModelProxy;
+
+class MemberLogProxy extends ModelProxy
+{
+}

+ 33 - 0
packages/Longyi/Member/src/Providers/EventServiceProvider.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Longyi\Member\Providers;
+
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\ServiceProvider;
+use Longyi\Member\Listeners\MemberHandler;
+use Longyi\Member\Listeners\VipDiscountHandler;
+
+class EventServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        Event::listen('checkout.cart.collect.totals.after', 'Longyi\Member\Listeners\MemberHandler@applyMemberDiscount');
+        Event::listen('checkout.cart.collect.totals.after', 'Longyi\Member\Listeners\VipDiscountHandler@applyVipDiscount');
+        Event::listen('bagisto.shop.checkout.onepage.summary.coupon.after', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::components.vip-member-discount-cartsummary');
+        });
+        Event::listen('bagisto.shop.checkout.cart.summary.vip_member_discount.after', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::components.member-discount-cartsummary');
+        });
+        Event::listen('bagisto.admin.sales.order.view.giftcard.before', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::sales.orders.view');
+        });
+        Event::listen('checkout.order.save.after', 'Longyi\Member\Listeners\OrderPlacedHandler@afterPlaceOrder');
+        Event::listen('bagisto.shop.customers.account.orders.view.information.discount.after', 'Longyi\Member\Listeners\OrderViewHandler@renderMemberInfo');
+    }
+}

+ 55 - 0
packages/Longyi/Member/src/Providers/MemberServiceProvider.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace Longyi\Member\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Event;
+use Longyi\Member\Providers\EventServiceProvider;
+
+class MemberServiceProvider extends ServiceProvider
+{
+    /**
+     * Register services.
+     */
+    public function register(): void
+    {
+        $this->registerConfig();
+    }
+
+    /**
+     * Bootstrap services.
+     */
+    public function boot(): void
+    {
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/shop-routes.php');
+
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'member');
+
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'member');
+
+        Event::listen('bagisto.admin.layout.head', function($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('member::admin.layouts.style');
+        });
+        $this->app->register(EventServiceProvider::class);
+    }
+
+    /**
+     * Register package config.
+     *
+     * @return void
+     */
+    protected function registerConfig()
+    {
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/admin-menu.php', 'menu.admin'
+        );
+
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/acl.php', 'acl'
+        );
+    }
+}

+ 15 - 0
packages/Longyi/Member/src/Providers/ModuleServiceProvider.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Longyi\Member\Providers;
+
+use Konekt\Concord\BaseModuleServiceProvider;
+
+class ModuleServiceProvider extends BaseModuleServiceProvider
+{
+    /**
+     * Models.
+     *
+     * @var array
+     */
+    protected $models = [];
+}

+ 16 - 0
packages/Longyi/Member/src/Repositories/MemberLogRepository.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Longyi\Member\Repositories;
+
+use Webkul\Core\Eloquent\Repository;
+
+class MemberLogRepository extends Repository
+{
+    /**
+     * Specify model class name.
+     */
+    public function model(): string
+    {
+        return 'Longyi\Member\Contracts\MemberLog';
+    }
+}

+ 23 - 0
packages/Longyi/Member/src/Resources/assets/css/app.css

@@ -0,0 +1,23 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+    .temp-icon {
+        width: 48px;
+        height: 48px;
+        display: inline-block;
+        background-size: cover;
+        background-image: url("../images/icon-temp.svg");
+    }
+
+    .active {
+        .temp-icon {
+            background-image: url("../images/icon-temp-active.svg");
+        }
+
+        &.temp-icon {
+            background-image: url("../images/icon-temp-active.svg");
+        }
+    }
+}

+ 12 - 0
packages/Longyi/Member/src/Resources/assets/images/icon-temp-active.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon-Catalog-Active</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Icon-Catalog-Active" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g transform="translate(9.000000, 7.000000)" stroke="#0041FF" stroke-width="2">
+            <rect id="Rectangle-2" x="0" y="0" width="30" height="34"></rect>
+        </g>
+    </g>
+</svg>

+ 12 - 0
packages/Longyi/Member/src/Resources/assets/images/icon-temp.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon-Catalog</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Icon-Catalog" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g transform="translate(9.000000, 7.000000)" stroke-width="2">
+            <rect id="Rectangle-2" stroke="#8E8E8E" x="0" y="0" width="30" height="34"></rect>
+        </g>
+    </g>
+</svg>

+ 4 - 0
packages/Longyi/Member/src/Resources/assets/js/app.js

@@ -0,0 +1,4 @@
+/**
+ * This will track all the images and fonts for publishing.
+ */
+import.meta.glob(["../images/**"]);

+ 9 - 0
packages/Longyi/Member/src/Resources/lang/en/app.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'member' => [
+        'discount' => 'Plus Fee',
+        'vip_active' => 'VIP 会员权益生效中',
+        'vip_discount' => 'VIP amount',
+    ],
+];

+ 9 - 0
packages/Longyi/Member/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'member' => [
+        'discount' => 'Plus Fee',
+        'vip_active' => 'VIP 会员权益生效中',
+        'vip_discount' => 'VIP amount',
+    ],
+];

+ 19 - 0
packages/Longyi/Member/src/Resources/views/admin/index.blade.php

@@ -0,0 +1,19 @@
+<x-admin::layouts>
+    <x-slot:title>
+        vip管理
+    </x-slot>
+
+    @if(session('success'))
+        <div class="mb-4 px-4 py-3 bg-green-500 text-white rounded-lg">
+            {{ session('success') }}
+        </div>
+    @endif
+
+    <div class="flex justify-between items-center">
+        <p class="text-xl text-gray-800 font-bold">
+            vip管理列表
+        </p>
+    </div>
+
+    <x-admin::datagrid src="{{ route('admin.member.index') }}"></x-admin::datagrid>
+</x-admin::layouts>

+ 1 - 0
packages/Longyi/Member/src/Resources/views/admin/layouts/style.blade.php

@@ -0,0 +1 @@
+<link rel="stylesheet" href="{{ asset('themes/default/assets/css/admin.css') }}">

+ 16 - 0
packages/Longyi/Member/src/Resources/views/components/member-discount-cartsummary.blade.php

@@ -0,0 +1,16 @@
+{!! view_render_event('bagisto.shop.checkout.cart.summary.member_discount.before') !!}
+
+<div
+    class="flex text-right justify-between mt-2"
+    v-if="cart.vip_discount_amount && parseFloat(cart.vip_discount_amount) > 0"
+>
+    <p class="text-base">
+        @lang('member::app.member.vip_discount')
+    </p>
+
+    <p class="text-base font-medium">
+        - @{{ cart.formatted_vip_discount_amount }}
+    </p>
+</div>
+
+{!! view_render_event('bagisto.shop.checkout.cart.summary.member_discount.after') !!}

+ 183 - 0
packages/Longyi/Member/src/Resources/views/components/vip-member-discount-cartsummary.blade.php

@@ -0,0 +1,183 @@
+{!! view_render_event('bagisto.shop.checkout.cart.summary.vip_member_discount.before') !!}
+
+<v-member-discount
+    :cart="cart"
+    @member-discount-toggled="getCart"
+>
+</v-member-discount>
+
+@pushOnce('scripts')
+    <script type="text/x-template" id="v-member-discount-template">
+        <!-- VIP 用户显示提示信息 -->
+        <div v-if="isVipUser" class="flex justify-between items-center mt-2 p-3 bg-green-50 border border-green-200 rounded-lg">
+            <div class="flex items-center gap-2">
+                <span class="text-green-600 text-xl">✓</span>
+                <div>
+                    <p class="text-base font-medium text-green-800">
+                        @lang('member::app.member.vip_active')
+                    </p>
+                    <p class="text-xs text-green-600">
+                        @{{ vipExpireMessage }}
+                    </p>
+                </div>
+            </div>
+        </div>
+
+        <!-- 非 VIP 或 VIP 已过期用户显示优惠开关 -->
+        <div v-else class="flex justify-between items-center mt-2">
+            <div class="flex items-center gap-2">
+                <p class="text-base">
+                    @lang('member::app.member.discount')
+                </p>
+
+                <!-- Toggle Switch -->
+                <div
+                    class="relative inline-flex items-center cursor-pointer"
+                    @click="toggleSwitch"
+                    :class="{ 'opacity-50 cursor-not-allowed': !canApplyDiscount }"
+                >
+                    <!-- Background -->
+                    <div
+                        class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out"
+                        :class="isMemberDiscountApplied ? 'bg-blue-600' : 'bg-gray-300'"
+                    ></div>
+
+                    <!-- Knob -->
+                    <div
+                        class="absolute left-0.5 top-0.5 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-200 ease-in-out border border-gray-300"
+                        :class="isMemberDiscountApplied ? 'transform translate-x-5' : ''"
+                    ></div>
+
+                    <!-- Hidden checkbox for form submission -->
+                    <input
+                        type="checkbox"
+                        class="sr-only"
+                        :checked="isMemberDiscountApplied"
+                        :disabled="!canApplyDiscount"
+                    >
+                </div>
+            </div>
+
+            <p class="text-base font-medium" v-if="isMemberDiscountApplied && cart.formatted_vip_plus_amount">
+                + @{{ cart.formatted_vip_plus_amount }}
+            </p>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-member-discount', {
+            template: '#v-member-discount-template',
+
+            props: ['cart'],
+
+            data() {
+                return {
+                    isMemberDiscountApplied: false,
+                };
+            },
+
+            computed: {
+                isVipUser() {
+                    const isVip = this.cart && this.cart.vip_status && this.cart.vip_status.is_vip;
+                    return isVip;
+                },
+
+                vipExpireMessage() {
+                    if (!this.cart || !this.cart.vip_status) {
+                        return '';
+                    }
+                    const days = this.cart.vip_status.days_remaining;
+                    if (days > 0) {
+                        return `VIP 有效期还剩 ${days} 天`;
+                    }
+                    return 'VIP 已激活';
+                },
+
+                canApplyDiscount() {
+                    return this.cart && parseFloat(this.cart.grand_total) > 19.9;
+                }
+            },
+
+            watch: {
+                cart: {
+                    handler(newCart) {
+                        if (newCart) {
+                            // 只同步状态,不自动应用
+                            this.isMemberDiscountApplied = !!(newCart.vip_plus_amount && parseFloat(newCart.vip_plus_amount) > 0);
+                        }
+                    },
+                    immediate: true,
+                    deep: true
+                }
+            },
+
+            methods: {
+                toggleSwitch() {
+                    if (this.isMemberDiscountApplied) {
+                        this.removeMemberDiscount();
+                    } else {
+                        this.applyMemberDiscount();
+                    }
+                },
+
+                applyMemberDiscount() {
+                    this.$axios.post("{{ route('shop.api.checkout.cart.member.apply') }}", {
+                        _token: "{{ csrf_token() }}"
+                    })
+                        .then((response) => {
+                            if (response.data.success) {
+                                this.isMemberDiscountApplied = true;
+                                this.$emitter.emit('add-flash', {
+                                    type: 'success',
+                                    message: response.data.message || '会员优惠已应用'
+                                });
+                                this.$emit('member-discount-toggled');
+                            } else {
+                                this.$emitter.emit('add-flash', {
+                                    type: 'error',
+                                    message: response.data.message || '应用会员优惠失败'
+                                });
+                            }
+                        })
+                        .catch((error) => {
+                            console.error('Apply member discount error:', error);
+                            this.$emitter.emit('add-flash', {
+                                type: 'error',
+                                message: error.response?.data?.message || '应用会员优惠失败'
+                            });
+                        });
+                },
+
+                removeMemberDiscount() {
+                    this.$axios.post("{{ route('shop.api.checkout.cart.member.remove') }}", {
+                        _token: "{{ csrf_token() }}"
+                    })
+                        .then((response) => {
+                            if (response.data.success) {
+                                this.isMemberDiscountApplied = false;
+                                this.$emitter.emit('add-flash', {
+                                    type: 'success',
+                                    message: response.data.message || '会员优惠已移除'
+                                });
+                                this.$emit('member-discount-toggled');
+                            } else {
+                                this.$emitter.emit('add-flash', {
+                                    type: 'error',
+                                    message: response.data.message || '移除会员优惠失败'
+                                });
+                            }
+                        })
+                        .catch((error) => {
+                            console.error('Remove member discount error:', error);
+                            this.$emitter.emit('add-flash', {
+                                type: 'error',
+                                message: error.response?.data?.message || '移除会员优惠失败'
+                            });
+                        });
+                }
+            }
+        });
+    </script>
+@endPushOnce
+
+{!! view_render_event('bagisto.shop.checkout.cart.summary.vip_member_discount.after') !!}

+ 22 - 0
packages/Longyi/Member/src/Resources/views/customers/account/orders/member-info.blade.php

@@ -0,0 +1,22 @@
+@if (isset($order) && $order->vip_plus_amount && $order->vip_plus_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.discount')
+        </p>
+
+        <p>
+            + {{ core()->formatPrice($order->vip_plus_amount, $order->order_currency_code) }}
+        </p>
+    </div>
+@endif
+@if (isset($order) && $order->vip_discount_amount && $order->vip_discount_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.vip_discount')
+        </p>
+
+        <p>
+            - {{ core()->formatPrice($order->vip_discount_amount, $order->order_currency_code) }}
+        </p>
+    </div>
+@endif

+ 29 - 0
packages/Longyi/Member/src/Resources/views/sales/orders/view.blade.php

@@ -0,0 +1,29 @@
+{{-- Member Discount (19.9) --}}
+@if (isset($order) && $order->vip_plus_amount && $order->vip_plus_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.discount')
+        </p>
+
+        <p>
+            + {{ core()->formatBasePrice($order->base_vip_plus_amount) }}
+        </p>
+    </div>
+@endif
+
+{!! view_render_event('bagisto.admin.sales.order.view.vip_plus.after') !!}
+
+{!! view_render_event('bagisto.admin.sales.order.view.vip_discount_amount.before') !!}
+{{-- VIP Discount (3%) --}}
+@if (isset($order) && $order->vip_discount_amount && $order->vip_discount_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p>
+            @lang('member::app.member.vip_discount')
+        </p>
+
+        <p>
+            - {{ core()->formatBasePrice($order->base_vip_discount_amount) }}
+        </p>
+    </div>
+@endif
+{!! view_render_event('bagisto.admin.sales.order.view.vip_discount_amount.after') !!}

+ 11 - 0
packages/Longyi/Member/src/Resources/views/shop/index.blade.php

@@ -0,0 +1,11 @@
+<x-shop::layouts>
+
+    <!-- Title of the page -->
+    <x-slot:title>
+        Package Member
+    </x-slot>
+
+    <div class="main">
+        Package Member
+    </div>
+</x-shop::layouts>

+ 10 - 0
packages/Longyi/Member/src/Routes/admin-routes.php

@@ -0,0 +1,10 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\Member\Http\Controllers\Admin\MemberController;
+
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin/member'], function () {
+    Route::controller(MemberController::class)->group(function () {
+        Route::get('', 'index')->name('admin.member.index');
+    });
+});

+ 14 - 0
packages/Longyi/Member/src/Routes/shop-routes.php

@@ -0,0 +1,14 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\Member\Http\Controllers\Shop\MemberController;
+use Longyi\Member\Http\Controllers\Shop\MemberDiscountController;
+
+Route::group(['middleware' => ['web', 'theme', 'locale', 'currency'], 'prefix' => 'member'], function () {
+    Route::get('', [MemberController::class, 'index'])->name('shop.member.index');
+});
+
+Route::group(['middleware' => ['web', 'theme', 'locale', 'currency'], 'prefix' => 'api/checkout/cart/member'], function () {
+    Route::post('apply', [MemberDiscountController::class, 'apply'])->name('shop.api.checkout.cart.member.apply');
+    Route::post('remove', [MemberDiscountController::class, 'remove'])->name('shop.api.checkout.cart.member.remove');
+});

+ 4 - 0
packages/Longyi/Member/tailwind.config.js

@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+    content: ["./src/Resources/**/*.blade.php", "./src/Resources/**/*.js"],
+};

+ 46 - 0
packages/Longyi/Member/vite.config.js

@@ -0,0 +1,46 @@
+import { defineConfig, loadEnv } from "vite";
+import vue from "@vitejs/plugin-vue";
+import laravel from "laravel-vite-plugin";
+import path from "path";
+
+export default defineConfig(({ mode }) => {
+    const envDir = "../../../";
+
+    Object.assign(process.env, loadEnv(mode, envDir));
+
+    return {
+        build: {
+            emptyOutDir: true,
+        },
+
+        envDir,
+
+        server: {
+            host: process.env.VITE_HOST || "localhost",
+            port: process.env.VITE_PORT || 5173,
+        },
+
+        plugins: [
+            vue(),
+
+            laravel({
+                hotFile: "../../../public/member-default-vite.hot",
+                publicDirectory: "../../../public",
+                buildDirectory: "themes/member/default/build",
+                input: [
+                    "src/Resources/assets/css/app.css",
+                    "src/Resources/assets/js/app.js",
+                ],
+                refresh: true,
+            }),
+        ],
+
+        experimental: {
+            renderBuiltUrl(filename, { hostId, hostType, type }) {
+                if (hostType === "css") {
+                    return path.basename(filename);
+                }
+            },
+        },
+    };
+});

+ 11 - 0
packages/Webkul/Customer/src/Models/Customer.php

@@ -107,7 +107,18 @@ class Customer extends Authenticatable implements CustomerContract
     {
         return ucfirst($this->first_name).' '.ucfirst($this->last_name);
     }
+    /**
+     * Get the isVip.
+     */
+    public function getIsVipAttribute(): string
+    {
+        if (!$this->vip_expire_date) {
+            return false;
+        }
 
+        $expireDate = \Carbon\Carbon::parse($this->vip_expire_date);
+        return !$expireDate->isPast();
+    }
     /**
      * Get image url for the customer image.
      *

+ 9 - 0
packages/Webkul/Sales/src/Transformers/OrderResource.php

@@ -77,6 +77,15 @@ class OrderResource extends JsonResource
                 'giftcard_amount'           =>  $this->giftcard_amount,
                 'base_giftcard_amount'           =>  $this->base_giftcard_amount,
             ]),
+            $this->mergeWhen($this->vip_plus_amount && $this->vip_plus_amount > 0, [
+                'vip_plus_amount'           => $this->vip_plus_amount,
+                'base_vip_plus_amount'      => $this->base_vip_plus_amount,
+            ]),
+            $this->mergeWhen($this->vip_discount_amount && $this->vip_discount_amount > 0, [
+                'vip_discount_amount'           => $this->vip_discount_amount,
+                'base_vip_discount_amount'      => $this->base_vip_discount_amount,
+                'formatted_vip_discount_amount' => core()->formatPrice($this->vip_discount_amount),
+            ]),
         ];
     }
 }

+ 51 - 0
packages/Webkul/Shop/src/Http/Resources/CartResource.php

@@ -4,6 +4,7 @@ namespace Webkul\Shop\Http\Resources;
 
 use Illuminate\Http\Resources\Json\JsonResource;
 use Webkul\Tax\Facades\Tax;
+use Carbon\Carbon;
 
 class CartResource extends JsonResource
 {
@@ -48,6 +49,7 @@ class CartResource extends JsonResource
             'have_stockable_items'               => $this->haveStockableItems(),
             'payment_method'                     => $this->payment?->method,
             'payment_method_title'               => core()->getConfigData('sales.payment_methods.'.$this->payment?->method.'.title'),
+            'vip_status'                         => $this->getVipStatus(),
             $this->mergeWhen($this->giftcard_number, [
                 'giftcard_number'           => $this->giftcard_number,
                 'giftcard_amount'           => $this->giftcard_amount,
@@ -56,6 +58,55 @@ class CartResource extends JsonResource
                 'remaining_giftcard_amount' => $this->remaining_giftcard_amount,
                 'formatted_remaining_giftcard_amount' => core()->formatPrice($this->remaining_giftcard_amount),
             ]),
+            $this->mergeWhen($this->vip_plus_amount && $this->vip_plus_amount > 0, [
+                'vip_plus_amount'           => $this->vip_plus_amount,
+                'base_vip_plus_amount'      => $this->base_vip_plus_amount,
+                'formatted_vip_plus_amount' => core()->formatPrice($this->vip_plus_amount),
+            ]),
+            $this->mergeWhen($this->vip_discount_amount && $this->vip_discount_amount > 0, [
+                'vip_discount_amount'           => $this->vip_discount_amount,
+                'base_vip_discount_amount'      => $this->base_vip_discount_amount,
+                'formatted_vip_discount_amount' => core()->formatPrice($this->vip_discount_amount),
+            ]),
+        ];
+
+    }
+    /**
+     * Get VIP status for current customer.
+     *
+     * @return array
+     */
+    protected function getVipStatus(): array
+    {
+        if (!auth()->guard('customer')->check()) {
+            return [
+                'is_vip' => false,
+                'vip_expire_date' => null,
+                'days_remaining' => 0,
+                'can_show_discount' => true,
+            ];
+        }
+
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer->vip_expire_date) {
+            return [
+                'is_vip' => false,
+                'vip_expire_date' => null,
+                'days_remaining' => 0,
+                'can_show_discount' => true,
+            ];
+        }
+
+        $expireDate = Carbon::parse($customer->vip_expire_date);
+        $isExpired = $expireDate->isPast();
+        $daysRemaining = $isExpired ? 0 : now()->diffInDays($expireDate, false);
+
+        return [
+            'is_vip' => !$isExpired,
+            'vip_expire_date' => $customer->vip_expire_date,
+            'days_remaining' => $daysRemaining,
+            'can_show_discount' => $isExpired,
         ];
     }
 }