فهرست منبع

Merge branch 'dev-rewardPoints' into dev

bianjunhui 1 هفته پیش
والد
کامیت
ffea6a3c3b
67فایلهای تغییر یافته به همراه7820 افزوده شده و 2 حذف شده
  1. 3 0
      .gitignore
  2. 2 0
      bootstrap/providers.php
  3. 2 0
      composer.json
  4. 23 0
      packages/Longyi/DynamicMenu/README.md
  5. 30 0
      packages/Longyi/DynamicMenu/composer.json
  6. 19 0
      packages/Longyi/DynamicMenu/src/Config/menu.php
  7. 154 0
      packages/Longyi/DynamicMenu/src/Console/Commands/InitializeSettings.php
  8. 38 0
      packages/Longyi/DynamicMenu/src/Database/Migrations/2026_01_01_000001_create_dynamic_menu_items_table.php
  9. 39 0
      packages/Longyi/DynamicMenu/src/Database/Seeders/MenuItemSeeder.php
  10. 48 0
      packages/Longyi/DynamicMenu/src/DynamicMenu.php
  11. 63 0
      packages/Longyi/DynamicMenu/src/DynamicMenuServiceProvider.php
  12. 189 0
      packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php
  13. 40 0
      packages/Longyi/DynamicMenu/src/Http/Middleware/CheckMenuPermission.php
  14. 59 0
      packages/Longyi/DynamicMenu/src/Models/MenuItem.php
  15. 17 0
      packages/Longyi/DynamicMenu/src/Models/MenuItemRole.php
  16. 117 0
      packages/Longyi/DynamicMenu/src/Providers/DynamicMenuServiceProvider.php
  17. 126 0
      packages/Longyi/DynamicMenu/src/Providers/MenuProvider.php
  18. 44 0
      packages/Longyi/DynamicMenu/src/Repositories/MenuItemRepository.php
  19. 43 0
      packages/Longyi/DynamicMenu/src/Resources/lang/en/app.php
  20. 37 0
      packages/Longyi/DynamicMenu/src/Resources/lang/zh_CN/app.php
  21. 224 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php
  22. 179 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/edit.blade.php
  23. 99 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/index.blade.php
  24. 44 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/partials/tree.blade.php
  25. 22 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/permission/index.blade.php
  26. 57 0
      packages/Longyi/DynamicMenu/src/Routes/admin-routes.php
  27. 49 0
      packages/Longyi/DynamicMenu/src/Services/MenuService.php
  28. 64 0
      packages/Longyi/RewardPoints/README.md
  29. 31 0
      packages/Longyi/RewardPoints/composer.json
  30. 43 0
      packages/Longyi/RewardPoints/src/Config/rewardpoints.php
  31. 43 0
      packages/Longyi/RewardPoints/src/Console/Commands/CheckExpiredPoints.php
  32. 117 0
      packages/Longyi/RewardPoints/src/Console/Commands/InitializeSettings.php
  33. 116 0
      packages/Longyi/RewardPoints/src/Database/Migrations/2026_01_01_000001_create_points_tables.php
  34. 18 0
      packages/Longyi/RewardPoints/src/Facades/CartRewardPoints.php
  35. 282 0
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/CustomerController.php
  36. 169 0
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/ReportController.php
  37. 490 0
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/RuleController.php
  38. 105 0
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/SettingController.php
  39. 202 0
      packages/Longyi/RewardPoints/src/Http/Controllers/Admin/TransactionController.php
  40. 200 0
      packages/Longyi/RewardPoints/src/Http/Controllers/RewardPointsController.php
  41. 118 0
      packages/Longyi/RewardPoints/src/Listeners/CustomerEvents.php
  42. 87 0
      packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php
  43. 33 0
      packages/Longyi/RewardPoints/src/Listeners/ReviewEvents.php
  44. 95 0
      packages/Longyi/RewardPoints/src/Models/RewardActiveRule.php
  45. 50 0
      packages/Longyi/RewardPoints/src/Models/RewardPointCustomer.php
  46. 33 0
      packages/Longyi/RewardPoints/src/Models/RewardPointCustomerSign.php
  47. 49 0
      packages/Longyi/RewardPoints/src/Models/RewardPointHistory.php
  48. 84 0
      packages/Longyi/RewardPoints/src/Models/RewardPointSetting.php
  49. 23 0
      packages/Longyi/RewardPoints/src/Providers/EventServiceProvider.php
  50. 143 0
      packages/Longyi/RewardPoints/src/Providers/RewardPointsServiceProvider.php
  51. 313 0
      packages/Longyi/RewardPoints/src/Repositories/RewardPointRepository.php
  52. 171 0
      packages/Longyi/RewardPoints/src/Repositories/RewardPointSettingRepository.php
  53. 156 0
      packages/Longyi/RewardPoints/src/Resources/lang/en/rewardpoints.php
  54. 105 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/customers/adjust-points.blade.php
  55. 223 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/customers/index.blade.php
  56. 121 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/customers/show.blade.php
  57. 198 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/reports/index.blade.php
  58. 327 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/rules/create.blade.php
  59. 325 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/rules/edit.blade.php
  60. 459 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/rules/index.blade.php
  61. 109 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/settings/index.blade.php
  62. 333 0
      packages/Longyi/RewardPoints/src/Resources/views/admin/transactions/index.blade.php
  63. 173 0
      packages/Longyi/RewardPoints/src/Resources/views/shop/customer/index.blade.php
  64. 169 0
      packages/Longyi/RewardPoints/src/Routes/routes.php
  65. 263 0
      packages/Longyi/RewardPoints/src/Services/CartRewardPoints.php
  66. 7 0
      packages/Webkul/Admin/src/Http/Controllers/Customers/CustomerController.php
  67. 6 2
      packages/Webkul/Admin/src/Listeners/Customer.php

+ 3 - 0
.gitignore

@@ -8,6 +8,9 @@
 *.hot
 /stubs
 /data
+/tmp
+/config/flexible_variant.php
+/config/octane.php
 /docker-compose-collection
 Homestead.json
 Homestead.yaml

+ 2 - 0
bootstrap/providers.php

@@ -11,6 +11,8 @@ return [
      */
     Webkul\Admin\Providers\AdminServiceProvider::class,
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
+    Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
+    Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
     Webkul\Attribute\Providers\AttributeServiceProvider::class,
     Webkul\BookingProduct\Providers\BookingProductServiceProvider::class,
     Webkul\CMS\Providers\CMSServiceProvider::class,

+ 2 - 0
composer.json

@@ -66,6 +66,8 @@
             "Database\\Factories\\": "database/factories/",
             "Database\\Seeders\\": "database/seeders/",
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
+            "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
+            "Longyi\\RewardPoints\\": "packages/Longyi/RewardPoints/src/",
             "Webkul\\Admin\\": "packages/Webkul/Admin/src",
             "Webkul\\Attribute\\": "packages/Webkul/Attribute/src",
             "Webkul\\BookingProduct\\": "packages/Webkul/BookingProduct/src",

+ 23 - 0
packages/Longyi/DynamicMenu/README.md

@@ -0,0 +1,23 @@
+# DynamicMenu - 动态菜单管理插件
+### 依赖关系
+
+- **PHP**: ^8.1|^8.2
+- **Laravel**: ^10.0|^11.0
+- **Bagisto**: 2.x
+
+---
+
+## 安装步骤
+
+### 1. 确认插件已注册
+确保 `bootstrap/providers.php` 文件中已添加:
+Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
+### 2. 运行数据库迁移
+php artisan migrate
+### 3. 初始化默认菜单
+php artisan dynamic-menu:init-settings
+### 4. 清理缓存
+php artisan cache:clear      # 清除应用缓存
+php artisan config:clear     # 清除配置缓存
+php artisan view:clear       # 清除视图缓存
+php artisan route:clear      # 清除路由缓存

+ 30 - 0
packages/Longyi/DynamicMenu/composer.json

@@ -0,0 +1,30 @@
+{
+    "name": "longyi/dynamicMenu",
+    "description": "Longyi Core Package for Bagisto - Flexible Variant Product Type with Option-Based Variants",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Longyi Team",
+            "email": "dev@longyi.com"
+        }
+    ],
+    "require": {
+        "php": "^8.1|^8.2",
+        "illuminate/support": "^10.0|^11.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Longyi\\DynamicMenu\\": "src/"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider"
+            ]
+        }
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true
+}

+ 19 - 0
packages/Longyi/DynamicMenu/src/Config/menu.php

@@ -0,0 +1,19 @@
+<?php
+
+return [
+    [
+        'key'   => 'settings.dynamicmenu',
+        'name'  => '测试菜单',
+        'route' => 'admin.dynamicmenu.index',
+        'sort' => 12,
+        'icon' => 'icon-menu',
+    ],
+    [
+        'key' => 'settings.dynamicmenu.menu',
+        'name' => '菜单项管理',
+        'route' => 'admin.dynamicmenu.index',
+        'sort' => 1,
+        'icon' => 'icon-list',
+        'parent' => 'settings.dynamicmenu'
+    ],
+];

+ 154 - 0
packages/Longyi/DynamicMenu/src/Console/Commands/InitializeSettings.php

@@ -0,0 +1,154 @@
+<?php
+
+namespace Longyi\DynamicMenu\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\DynamicMenu\Models\MenuItem;
+use Illuminate\Support\Facades\Schema;
+
+class InitializeSettings extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'dynamic-menu:init-settings {--force : Force recreate menu items}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Initialize dynamic menu settings and create default menu items';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $this->info('Initializing dynamic menu settings...');
+
+        try {
+            // 检查数据库表是否存在
+            if (!Schema::hasTable('dynamic_menu_items')) {
+                $this->error('Table "dynamic_menu_items" not found.');
+                $this->warn('Please run migrations first: php artisan migrate');
+                return 1;
+            }
+
+            // 创建默认菜单项
+            $this->createDefaultMenuItems();
+
+            $this->info('✓ Dynamic menu initialized successfully!');
+            $this->info('You can now manage menu items in admin panel.');
+
+            return 0;
+
+        } catch (\Exception $e) {
+            $this->error('Error initializing dynamic menu: ' . $e->getMessage());
+            \Log::error('DynamicMenu initialization error: ' . $e->getMessage() . "\n" . $e->getTraceAsString());
+            return 1;
+        }
+    }
+
+    /**
+     * 创建默认菜单项
+     */
+    protected function createDefaultMenuItems()
+    {
+        $this->info('Creating default menu items...');
+
+        // 检查是否已存在动态菜单的菜单项
+        $existingParent = MenuItem::where('key', 'dynamicmenu')->first();
+
+        if ($existingParent) {
+            $this->warn('Dynamic menu items already exist.');
+
+            if (!$this->option('force')) {
+                $answer = $this->ask('Do you want to recreate them? (y/n)', 'n');
+                if (strtolower($answer) !== 'y') {
+                    return;
+                }
+            }
+
+            // 删除已存在的菜单项
+            $this->deleteExistingMenuItems($existingParent);
+        }
+
+        // 获取当前管理员 ID
+        $adminId = auth()->guard('admin')->check() ? auth()->guard('admin')->id() : 1;
+
+        // 创建父级菜单项
+        $parentItem = MenuItem::create([
+            'name' => '自定义菜单',
+            'key' => 'dynamicmenu',
+            'route' => 'admin.dynamicmenu.index',
+            'icon' => 'icon-menu',
+            'sort_order' => 13,
+            'status' => true,
+            'created_by' => $adminId,
+        ]);
+
+        $this->info("✓ Parent menu created: {$parentItem->name}");
+
+        // 创建子菜单项
+        $childItems = [
+            [
+                'name' => '菜单列表',
+                'key' => 'dynamicmenu.menu',
+                'route' => 'admin.dynamicmenu.index',
+                'icon' => 'icon-list',
+                'sort_order' => 1,
+            ],
+            [
+                'name' => '权限管理',
+                'key' => 'dynamicmenu.permission',
+                'route' => 'admin.dynamicmenu.permission',
+                'icon' => 'icon-lock',
+                'sort_order' => 2,
+            ],
+        ];
+
+        foreach ($childItems as $item) {
+            MenuItem::create([
+                'name' => $item['name'],
+                'key' => $item['key'],
+                'route' => $item['route'],
+                'icon' => $item['icon'],
+                'sort_order' => $item['sort_order'],
+                'parent_id' => $parentItem->id,
+                'status' => true,
+                'created_by' => $adminId,
+            ]);
+            $this->info("  ✓ Child menu created: {$item['name']}");
+        }
+
+        $this->info('✓ All menu items created successfully!');
+    }
+
+    /**
+     * 删除已存在的菜单项
+     */
+    protected function deleteExistingMenuItems($menuItem)
+    {
+        $this->info('Removing existing menu items...');
+
+        try {
+            // 先删除所有子菜单
+            $children = MenuItem::where('parent_id', $menuItem->id)->get();
+            foreach ($children as $child) {
+                $child->delete();
+            }
+
+            // 删除父菜单
+            $menuItem->delete();
+
+            $this->info('✓ Existing menu items deleted.');
+
+        } catch (\Exception $e) {
+            $this->error('Failed to delete existing menu items: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+}

+ 38 - 0
packages/Longyi/DynamicMenu/src/Database/Migrations/2026_01_01_000001_create_dynamic_menu_items_table.php

@@ -0,0 +1,38 @@
+<?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('dynamic_menu_items', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->string('key')->unique();
+            $table->string('route');
+            $table->string('icon')->nullable();
+            $table->unsignedBigInteger('parent_id')->nullable();
+            $table->integer('sort_order')->default(0);
+            $table->boolean('status')->default(true);
+            $table->unsignedBigInteger('created_by')->nullable();
+            $table->timestamps();
+            // Indexes
+            $table->index('sort_order', 'idx_order');
+           
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('dynamic_menu_items');
+    }
+};

+ 39 - 0
packages/Longyi/DynamicMenu/src/Database/Seeders/MenuItemSeeder.php

@@ -0,0 +1,39 @@
+<?php
+namespace Longyi\DynamicMenu\Database\Seeders;  // 修改这里
+use Illuminate\Database\Seeder;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class MenuItemSeeder extends Seeder
+{
+    public function run()
+    {
+        $dynamicMenu = MenuItem::create([
+            'name' => '自定义菜单',
+            'key' => 'dynamicmenu',
+            'route' => 'admin.dynamicmenu.index',
+            'icon' => 'icon-menu',
+            'sort_order' => 13,
+            'status' => true
+        ]);
+
+        MenuItem::create([
+            'name' => '菜单列表',
+            'key' => 'dynamicmenu.menu',
+            'route' => 'admin.dynamicmenu.index',
+            'icon' => 'icon-list',
+            'parent_id' => $dynamicMenu->id,
+            'sort_order' => 1,
+            'status' => true
+        ]);
+
+        MenuItem::create([
+            'name' => '权限管理',
+            'key' => 'dynamicmenu.permission',
+            'route' => 'admin.dynamicmenu.permission',
+            'icon' => 'icon-lock',
+            'parent_id' => $dynamicMenu->id,
+            'sort_order' => 2,
+            'status' => true
+        ]);
+    }
+}

+ 48 - 0
packages/Longyi/DynamicMenu/src/DynamicMenu.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace Longyi\DynamicMenu;
+
+use Longyi\DynamicMenu\Models\MenuItem as MenuItemModel;
+use Illuminate\Support\Facades\Route;
+
+class DynamicMenu
+{
+    public function registerMenuItems()
+    {
+        try {
+            $items = MenuItemModel::where('status', 1)
+                ->orderBy('parent_id')
+                ->orderBy('sort_order')
+                ->get();
+                
+            foreach ($items as $item) {
+                $this->addToBagistoMenu($item);
+            }
+        } catch (\Exception $e) {
+            report($e);
+        }
+    }
+    
+    private function addToBagistoMenu($item)
+    {
+        $parent = $item->parent ? $item->parent->key : null;
+        
+        // 方法1:使用路由名称(推荐)
+        // Bagisto 会在内部使用 route() 辅助函数生成URL
+        menu()->addItem(
+            $item->key,
+            $item->name,
+            $parent,
+            $item->route,  // 这里应该是路由名称,如 'admin.dynamicmenu.index'
+            $item->icon,
+            (int) $item->sort_order
+        );
+        
+        // 调试日志
+        \Log::info('注册菜单项', [
+            'key' => $item->key,
+            'route_name' => $item->route,
+            'generated_url' => route($item->route, [], false)
+        ]);
+    }
+}

+ 63 - 0
packages/Longyi/DynamicMenu/src/DynamicMenuServiceProvider.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Longyi\DynamicMenu;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Routing\Router;
+use Longyi\DynamicMenu\Http\Middleware\CheckMenuPermission;
+
+class DynamicMenuServiceProvider extends ServiceProvider
+{
+    public function boot(Router $router)
+    {
+        // Load routes
+        $this->loadRoutesFrom(__DIR__ . '/Routes/admin-routes.php');
+
+        // Load views
+        $this->loadViewsFrom(__DIR__ . '/Resources/views', 'dynamicmenu');
+
+        // Load translations
+        $this->loadTranslationsFrom(__DIR__ . '/Resources/lang', 'dynamicmenu');
+
+        // Load migrations
+        $this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
+
+        // Register middleware
+        $router->aliasMiddleware('menu.permission', CheckMenuPermission::class);
+
+        // Publish assets
+        $this->publishes([
+            __DIR__ . '/Resources/views' => resource_path('views/vendor/longyi/dynamicmenu'),
+        ], 'dynamicmenu-views');
+
+        $this->publishes([
+            __DIR__ . '/Database/Seeders' => database_path('seeders'),
+        ], 'dynamicmenu-seeds');
+
+        $this->publishes([
+            __DIR__ . '/Config' => config_path('longyi'),
+        ], 'dynamicmenu-config');
+
+        // Register menu items
+        $this->app->booted(function () {
+            $this->registerMenuItems();
+        });
+    }
+
+    public function register()
+    {
+        $this->mergeConfigFrom(__DIR__ . '/Config/menu.php', 'dynamicmenu');
+
+        $this->app->singleton('dynamicmenu', function () {
+            return new DynamicMenu();
+        });
+    }
+
+    private function registerMenuItems()
+    {
+        if (app('request')->is('admin*')) {
+            $dynamicMenu = app('dynamicmenu');
+            $dynamicMenu->registerMenuItems();
+        }
+    }
+}

+ 189 - 0
packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php

@@ -0,0 +1,189 @@
+<?php
+
+namespace Longyi\DynamicMenu\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller;
+use Longyi\DynamicMenu\Models\MenuItem;
+use Webkul\User\Repositories\RoleRepository;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\View;
+
+class MenuController extends Controller
+{
+    protected $roleRepository;
+    
+    public function __construct(RoleRepository $roleRepository)
+    {
+        $this->roleRepository = $roleRepository;
+        $this->_config = request('_config');
+    }
+    
+    
+    public function index()
+    {
+        try {
+            // 直接从数据库获取最新数据,不使用缓存
+            $allItems = MenuItem::with('children')
+                ->orderBy('parent_id', 'ASC')
+                ->orderBy('sort_order', 'ASC')
+                ->get();
+            
+            // 获取顶级菜单项
+             $menuItems = $allItems->filter(function($item) {
+                return is_null($item->parent_id) || $item->parent_id == 0;
+            })->values();
+            
+            
+            return view('dynamicmenu::admin.menu.index', compact('menuItems', 'allItems'));
+            
+        } catch (\Exception $e) {
+            \Log::error('MenuController 错误:' . $e->getMessage());
+            \Log::error($e->getTraceAsString());
+            
+            session()->flash('error', '加载菜单列表失败:' . $e->getMessage());
+            return redirect()->route('admin.dashboard.index');
+        }
+    }
+    
+    
+    public function create()
+    {
+        $menuItems = MenuItem::orderBy('parent_id')->orderBy('sort_order')->get();
+        return view($this->_config['view'], compact('menuItems'));
+    }
+    
+    public function store(Request $request)
+    {
+        
+        $request->validate([
+            'name' => 'required|string|max:255',
+            'key' => 'required|string|unique:dynamic_menu_items,key',
+            'route' => 'required|string',
+            'icon' => 'nullable|string',
+            'parent_id' => 'nullable|exists:dynamic_menu_items,id',
+            'sort_order' => 'integer',
+            'status' => 'boolean'
+        ]);
+        
+        $data = $request->all();
+        $data['parent_id'] = $data['parent_id'] === '' ? null : $data['parent_id'];
+        $data['created_by'] = auth()->guard('admin')->user()->id;
+        
+        MenuItem::create($data);
+        
+        session()->flash('success', '菜单项创建成功');
+        
+        return redirect()->route($this->_config['redirect']);
+    }
+    
+    public function edit($id)
+    {
+        try {
+            $menuItem = MenuItem::with('parent')->findOrFail($id);
+            $menuItems = MenuItem::orderBy('parent_id')->orderBy('sort_order')->get();
+            
+            return view($this->_config['view'], compact('menuItem', 'menuItems'));
+            
+        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
+            session()->flash('error', '菜单项不存在');
+            return redirect()->route('admin.dynamicmenu.index');
+        }
+    }
+    
+    public function update(Request $request, $id)
+    {
+        try {
+            $request->validate([
+                'name' => 'required|string|max:255',
+                'key' => 'required|string|unique:dynamic_menu_items,key,' . $id,
+                'route' => 'required|string',
+                'icon' => 'nullable|string',
+                'parent_id' => 'nullable|exists:dynamic_menu_items,id',
+                'sort_order' => 'integer',
+                'status' => 'boolean'
+            ]);
+            
+            $menuItem = MenuItem::findOrFail($id);
+            $menuItem->update($request->all());
+            
+            session()->flash('success', '菜单项更新成功');
+            
+            return redirect()->route($this->_config['redirect']);
+            
+        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
+            session()->flash('error', '菜单项不存在');
+            return redirect()->back();
+        } catch (\Exception $e) {
+            \Log::error('更新菜单项失败:' . $e->getMessage());
+            session()->flash('error', '更新失败:' . $e->getMessage());
+            return redirect()->back()->withInput();
+        }
+    }
+    
+     public function destroy($id)
+    {
+        try {
+            $menuItem = MenuItem::with('children')->findOrFail($id);
+            
+            // 检查是否有子菜单
+            if ($menuItem->children && $menuItem->children->count() > 0) {
+                session()->flash('error', '请先删除子菜单项');
+                return redirect()->back();
+            }
+            
+            $menuItem->delete();
+            
+            session()->flash('success', '菜单项删除成功');
+            
+            return redirect()->route($this->_config['redirect']);
+            
+        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
+            session()->flash('error', '菜单项不存在');
+            return redirect()->back();
+        } catch (\Exception $e) {
+            \Log::error('删除菜单项失败:' . $e->getMessage());
+            session()->flash('error', '删除失败:' . $e->getMessage());
+            return redirect()->back();
+        }
+    }
+    
+    public function permission()
+    {
+        $roles = $this->roleRepository->all();
+        $menuItems = MenuItem::with('children')->orderBy('parent_id')->orderBy('sort_order')->get();
+        
+        return view($this->_config['view'], compact('roles', 'menuItems'));
+    }
+    
+    public function updatePermission(Request $request)
+    {
+        $roleId = $request->input('role_id');
+        $menuItemIds = $request->input('menu_items', []);
+        
+        DB::table('menu_item_role')->where('role_id', $roleId)->delete();
+        
+        foreach ($menuItemIds as $menuItemId) {
+            DB::table('menu_item_role')->insert([
+                'menu_item_id' => $menuItemId,
+                'role_id' => $roleId
+            ]);
+        }
+        
+        session()->flash('success', '权限更新成功');
+        
+        return redirect()->back();
+    }
+    
+    public function getRolePermissions(Request $request)
+    {
+        $roleId = $request->input('role_id');
+        
+        $permissions = DB::table('menu_item_role')
+            ->where('role_id', $roleId)
+            ->pluck('menu_item_id')
+            ->toArray();
+            
+        return response()->json($permissions);
+    }
+}

+ 40 - 0
packages/Longyi/DynamicMenu/src/Http/Middleware/CheckMenuPermission.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Longyi\DynamicMenu\Http\Middleware;
+
+use Closure;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class CheckMenuPermission
+{
+    public function handle($request, Closure $next)
+    {
+        if (! auth()->guard('admin')->check()) {
+            return $next($request);
+        }
+
+        $admin = auth()->guard('admin')->user();
+        $routeName = $request->route()->getName();
+
+        // Skip check for super admin
+        if ($admin->role->permission_type === 'all') {
+            return $next($request);
+        }
+
+        // Find menu item by route
+        $menuItem = MenuItem::where('route', $routeName)->first();
+
+        if ($menuItem) {
+            $hasPermission = $admin->role->menuItems()
+                ->where('menu_item_id', $menuItem->id)
+                ->exists();
+
+            if (! $hasPermission) {
+                session()->flash('warning', trans('dynamicmenu::app.permission.denied'));
+                return redirect()->route('admin.dashboard.index');
+            }
+        }
+
+        return $next($request);
+    }
+}

+ 59 - 0
packages/Longyi/DynamicMenu/src/Models/MenuItem.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Longyi\DynamicMenu\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Cache;
+class MenuItem extends Model
+{
+    protected $table = 'dynamic_menu_items';
+    
+    protected $fillable = [
+        'name',
+        'key',
+        'route',
+        'icon',
+        'parent_id',
+        'sort_order',
+        'status',
+        'created_by'
+    ];
+    
+    protected $casts = [
+        'status' => 'boolean',
+        'sort_order' => 'integer'
+    ];
+    protected static function boot()
+    {
+        parent::boot();
+        
+        // 当菜单项被创建、更新或删除时,清除缓存
+        static::saved(function () {
+            Cache::forget('dynamic_menu_config');
+            Cache::forget('dynamic_menu_items');
+            \Log::info('菜单缓存已清除 (saved)');
+        });
+        
+        static::deleted(function () {
+            Cache::forget('dynamic_menu_config');
+            Cache::forget('dynamic_menu_items');
+            \Log::info('菜单缓存已清除 (deleted)');
+        });
+    }
+
+    /**
+     * 获取父菜单
+     */
+    public function parent()
+    {
+        return $this->belongsTo(MenuItem::class, 'parent_id');
+    }
+    
+    /**
+     * 获取子菜单
+     */
+    public function children()
+    {
+        return $this->hasMany(MenuItem::class, 'parent_id')->orderBy('sort_order');
+    }
+}

+ 17 - 0
packages/Longyi/DynamicMenu/src/Models/MenuItemRole.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Longyi\DynamicMenu\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class MenuItemRole extends Model
+{
+    protected $table = 'menu_item_role';
+
+    protected $fillable = [
+        'menu_item_id',
+        'role_id'
+    ];
+
+    public $timestamps = false;
+}

+ 117 - 0
packages/Longyi/DynamicMenu/src/Providers/DynamicMenuServiceProvider.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace Longyi\DynamicMenu\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Cache;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class DynamicMenuServiceProvider extends ServiceProvider
+{
+    public function boot()
+    {
+        // 加载迁移
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
+        // 加载路由
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+
+        // 加载视图
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'dynamicmenu');
+         // 加载语言文件
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'dynamicmenu');
+        // 发布资源
+        $this->publishes([
+            __DIR__ . '/../../publishable/assets' => public_path('vendor/dynamicmenu'),
+        ], 'public');
+    }
+
+    public function register()
+    {
+        // 延迟合并配置,等到服务提供者注册完成后
+        $this->app->booted(function () {
+            $this->mergeDynamicMenuConfig();
+        });
+        if ($this->app->runningInConsole()) {
+            $this->commands([
+                \Longyi\DynamicMenu\Console\Commands\InitializeSettings::class,
+            ]);
+        }
+    }
+
+    /**
+     * 合并动态菜单配置
+     */
+    protected function mergeDynamicMenuConfig()
+    {
+        try {
+            // 从数据库获取菜单配置
+            $menuConfig = $this->getMenuConfigFromDatabase();
+            // 获取现有配置
+            $existingConfig = $this->app['config']->get('menu.admin', []);
+            // 合并配置
+            $mergedConfig = array_merge($existingConfig, $menuConfig);
+            // 设置配置
+            $this->app['config']->set('menu.admin', $mergedConfig);
+
+        } catch (\Exception $e) {
+            \Log::error('合并动态菜单配置失败: ' . $e->getMessage());
+        }
+    }
+
+
+    /**
+     * 从数据库获取菜单配置
+     */
+    protected function getMenuConfigFromDatabase($useCache = true)
+    {
+        if (!$useCache) {
+            // 不使用缓存,直接查询数据库
+            $menuItems = MenuItem::with('children')
+                        ->where('status', 1)
+                        ->orderBy('sort_order')
+                        ->get();
+
+            return $this->buildMenuConfig($menuItems);
+        }
+
+        // 使用缓存
+        return Cache::remember('dynamic_menu_config', 3600, function () {
+            try {
+                $menuItems = MenuItem::with('children')
+                    ->where('status', 1)
+                    ->orderBy('sort_order')
+                    ->get();
+
+                return $this->buildMenuConfig($menuItems);
+
+            } catch (\Exception $e) {
+                \Log::warning('无法从数据库获取菜单:' . $e->getMessage());
+                return [];
+            }
+        });
+    }
+
+// ... existing code ...
+
+    /**
+     * 构建菜单配置数组
+     */
+    protected function buildMenuConfig($menuItems)
+    {
+        $config = [];
+
+        foreach ($menuItems as $item) {
+            $menuItem = [
+                'key'   => $item->key,
+                'name'  => $item->name,
+                'route' => $item->route ?: 'admin.dynamicmenu.index',
+                'sort'  => (int) $item->sort_order,
+                'icon'  => $item->icon ?: 'icon-menu',
+            ];
+            $config[] = $menuItem;
+        }
+
+        return $config;
+    }
+}

+ 126 - 0
packages/Longyi/DynamicMenu/src/Providers/MenuProvider.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace Longyi\DynamicMenu\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Longyi\DynamicMenu\Services\MenuService;
+
+class MenuProvider extends ServiceProvider
+{
+    public function boot(MenuService $menuService)
+    {
+        // 在 Bagisto 菜单构建时添加动态菜单
+        $this->app['events']->listen('bagisto.admin.menu.build', function ($menu) use ($menuService) {
+            $this->registerDynamicMenus($menu, $menuService);
+        });
+    }
+    
+    /**
+     * 注册动态菜单到 Bagisto
+     */
+    protected function registerDynamicMenus($menu, MenuService $menuService)
+    {
+        // 获取所有顶级菜单
+        $topLevelMenus = $menuService->buildMenuTree();
+        
+        foreach ($topLevelMenus as $menuItem) {
+            // 检查菜单是否应该显示在 Settings 下
+            if (strpos($menuItem['key'], 'settings.') === 0) {
+                // 如果菜单key以settings.开头,添加到Settings菜单下
+                $this->addToSettingsMenu($menu, $menuItem);
+            } else {
+                // 否则作为顶级菜单添加
+                $this->addMenuItem($menu, null, $menuItem);
+            }
+        }
+    }
+    
+    /**
+     * 添加菜单项到指定父菜单
+     */
+    protected function addMenuItem($menu, $parentKey = null, $item)
+    {
+        if (isset($item['children']) && count($item['children']) > 0) {
+            // 有子菜单
+            $parent = $menu->add(
+                $item['key'],
+                $item['name'],
+                $item['route'],
+                $item['sort'],
+                $item['icon']
+            );
+            
+            // 添加子菜单
+            foreach ($item['children'] as $child) {
+                $this->addMenuItem($parent, $item['key'], $child);
+            }
+        } else {
+            // 无子菜单
+            if ($parentKey) {
+                // 添加到指定父菜单
+                $menu->add(
+                    $item['key'],
+                    $item['name'],
+                    $item['route'],
+                    $item['sort'],
+                    $item['icon'],
+                    $parentKey
+                );
+            } else {
+                // 作为顶级菜单
+                $menu->add(
+                    $item['key'],
+                    $item['name'],
+                    $item['route'],
+                    $item['sort'],
+                    $item['icon']
+                );
+            }
+        }
+    }
+    
+    /**
+     * 添加到 Settings 菜单
+     */
+    protected function addToSettingsMenu($menu, $item)
+    {
+        // 确保 Settings 菜单存在
+        $settings = $menu->get('settings');
+        
+        if (!$settings) {
+            // 如果 Settings 菜单不存在,先创建
+            $settings = $menu->add('settings', 'Settings', 'admin.settings.index', 1, 'icon-settings');
+        }
+        
+        if (isset($item['children']) && count($item['children']) > 0) {
+            // 有子菜单
+            $parent = $settings->addChild(
+                $item['key'],
+                $item['name'],
+                $item['route'],
+                $item['sort'],
+                $item['icon']
+            );
+            
+            // 添加子菜单
+            foreach ($item['children'] as $child) {
+                $parent->addChild(
+                    $child['key'],
+                    $child['name'],
+                    $child['route'],
+                    $child['sort'],
+                    $child['icon']
+                );
+            }
+        } else {
+            // 无子菜单,直接添加到 Settings
+            $settings->addChild(
+                $item['key'],
+                $item['name'],
+                $item['route'],
+                $item['sort'],
+                $item['icon']
+            );
+        }
+    }
+}

+ 44 - 0
packages/Longyi/DynamicMenu/src/Repositories/MenuItemRepository.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Longyi\DynamicMenu\Repositories;
+
+use Webkul\Core\Eloquent\Repository;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class MenuItemRepository extends Repository
+{
+    public function model()
+    {
+        return MenuItem::class;
+    }
+
+    public function getMenuTree()
+    {
+        $items = $this->all();
+        return $this->buildTree($items);
+    }
+
+    private function buildTree($items, $parentId = null)
+    {
+        $tree = [];
+
+        foreach ($items as $item) {
+            if ($item->parent_id == $parentId) {
+                $children = $this->buildTree($items, $item->id);
+                if ($children) {
+                    $item->children = $children;
+                }
+                $tree[] = $item;
+            }
+        }
+
+        return $tree;
+    }
+
+    public function getMenuItemsForRole($roleId)
+    {
+        return $this->model->whereHas('roles', function($query) use ($roleId) {
+            $query->where('role_id', $roleId);
+        })->where('status', 1)->get();
+    }
+}

+ 43 - 0
packages/Longyi/DynamicMenu/src/Resources/lang/en/app.php

@@ -0,0 +1,43 @@
+<?php
+
+return [
+    'admin' => [
+        'menu' => [
+            'title' => 'Dynamic Menu',
+            'items' => 'Menu Items',
+            'permissions' => 'Menu Permissions',
+            'add-title' => 'Add Menu Item',
+            'edit-title' => 'Edit Menu Item',
+            'add-btn' => 'Add Menu Item',
+            'back-btn' => 'Back',
+            'save-btn' => 'Save',
+            'cancel-btn' => 'Cancel',
+            'basic-info' => 'Basic Information',
+            'name' => 'Name',
+            'name-placeholder' => 'e.g. Dashboard',
+            'key' => 'Key',
+            'key-placeholder' => 'e.g. admin.dashboard',
+            'key-info' => 'Used for permission validation and menu activation',
+            'route' => 'URL/Route',
+            'route-placeholder' => 'e.g. admin.dashboard',
+            'route-info' => 'Can be relative path, full URL or route name',
+            'icon' => 'Icon',
+            'icon-placeholder' => 'e.g. icon-menu',
+            'icon-info' => 'Webkul admin panel icon class',
+            'parent' => 'Parent Menu',
+            'parent-none' => 'As Top Level Menu',
+            'has-children' => 'Has Children',
+            'sort-order' => 'Sort Order',
+            'sort-order-info' => 'Lower number appears first',
+            'status' => 'Status',
+            'status-active' => 'Active',
+            'status-inactive' => 'Inactive',
+            'edit' => 'Edit',
+            'delete' => 'Delete',
+            'delete-confirm' => 'Are you sure?',
+            'delete-disabled' => 'Please delete child menus first',
+            'disabled' => 'Disabled',
+            'no-identifier' => 'No identifier',
+        ],
+    ],
+];

+ 37 - 0
packages/Longyi/DynamicMenu/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,37 @@
+<?php
+
+return [
+    'admin' => [
+        'menu' => [
+            'title' => '动态菜单',
+            'items' => '菜单项',
+            'permissions' => '菜单权限',
+            'add-title' => '添加菜单项',
+            'edit-title' => '编辑菜单项',
+            'add-btn' => '添加菜单项',
+            'back-btn' => '返回列表',
+            'save-btn' => '保存菜单项',
+            'cancel-btn' => '取消',
+            'basic-info' => '基本信息',
+            'name' => '名称',
+            'name-placeholder' => '例如:仪表盘',
+            'key' => 'Key',
+            'key-placeholder' => '例如:admin.dashboard',
+            'key-info' => '用于权限验证和菜单激活状态判断',
+            'route' => 'URL/路由',
+            'route-placeholder' => '例如:admin.dashboard',
+            'route-info' => '可以是相对路径、完整URL或路由名称',
+            'icon' => '图标',
+            'icon-placeholder' => '例如:icon-menu',
+            'icon-info' => 'Webkul admin面板的图标类名',
+            'parent' => '父级菜单',
+            'parent-none' => '作为顶级菜单',
+            'has-children' => '有子菜单',
+            'sort-order' => '排序',
+            'sort-order-info' => '数字越小越靠前',
+            'status' => '状态',
+            'status-active' => '启用',
+            'status-inactive' => '禁用',
+        ],
+    ],
+];

+ 224 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php

@@ -0,0 +1,224 @@
+<x-admin::layouts>
+    {{-- 页面标题 --}}
+    <x-slot:title>添加菜单项</x-slot:title>
+
+    {{-- 页面内容 --}}
+    <div class="content" style="padding: 20px;">
+        {{-- 页头部分 --}}
+        <div class="page-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #dee2e6;">
+            <div class="page-title">
+                <h1><i class="fas fa-plus-circle"></i> 添加菜单项</h1>
+            </div>
+            <div class="page-action">
+                <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 8px 16px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;">
+                    <i class="fas fa-arrow-left"></i> 返回列表
+                </a>
+            </div>
+        </div>
+
+        {{-- 显示错误信息 --}}
+        @if($errors->any())
+            <div class="alert alert-danger" style="background: #f8d7da; color: #721c24; padding: 12px; border-radius: 4px; margin-bottom: 20px; border: 1px solid #f5c6cb;">
+                <ul class="mb-0" style="margin: 0; padding-left: 20px;">
+                    @foreach($errors->all() as $error)
+                        <li>{{ $error }}</li>
+                    @endforeach
+                </ul>
+            </div>
+        @endif
+
+        {{-- 表单面板 --}}
+        <div class="panel" style="background: white; border: 1px solid #dee2e6; border-radius: 4px; margin-bottom: 20px;">
+            <div class="panel-header" style="padding: 12px 20px; background: #f8f9fa; border-bottom: 1px solid #dee2e6;">
+                <h3 style="margin: 0; font-size: 18px;">基本信息</h3>
+            </div>
+
+            <div class="panel-body" style="padding: 20px;">
+                <form method="POST" action="{{ route('admin.dynamicmenu.store') }}">
+                    @csrf
+
+                    {{-- 名称 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label class="required" style="display: block; margin-bottom: 5px; font-weight: 500;">
+                            名称 <span style="color: red;">*</span>
+                        </label>
+                        <input type="text"
+                               name="name"
+                               class="control"
+                               value="{{ old('name') }}"
+                               required
+                               placeholder="例如:仪表盘"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                    </div>
+
+                    {{-- Key --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label class="required" style="display: block; margin-bottom: 5px; font-weight: 500;">
+                            Key <span style="color: red;">*</span>
+                        </label>
+                        <input type="text"
+                            name="key"
+                            class="control"
+                            value="{{ old('key') }}"
+                            required
+                            placeholder="例如:settings.dynamic-menu"
+                            style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            <strong style="color: #dc3545;">重要提示:</strong>
+                            如果想让菜单显示在 Settings 下,Key 必须以 <strong style="background: #f0f0f0; padding: 2px 5px; border-radius: 3px;">settings.</strong> 开头<br>
+                            例如:<code>settings.dynamic-menu</code>、<code>settings.my-menu</code>、<code>settings.custom</code>
+                        </small>
+                    </div>
+
+                    {{-- URL/路由 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">URL/路由</label>
+                        <input type="text"
+                               name="route"
+                               class="control"
+                               value="{{ old('route') }}"
+                               placeholder="例如:admin.dashboard"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            可以是相对路径、完整URL或路由名称
+                        </small>
+                    </div>
+
+                    {{-- 图标 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">图标</label>
+                        <input type="text"
+                               name="icon"
+                               class="control"
+                               value="{{ old('icon', 'fas fa-file') }}"
+                               placeholder="例如:fas fa-dashboard、fas fa-users等"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            FontAwesome图标类名,如:fas fa-home
+                        </small>
+                    </div>
+
+                    {{-- 父级菜单 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">父级菜单</label>
+                        <select name="parent_id" class="control" style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                            <option value="">作为顶级菜单</option>
+                            @foreach($menuItems as $menuItem)
+                                <option value="{{ $menuItem->id }}" {{ old('parent_id') == $menuItem->id ? 'selected' : '' }}>
+                                    {{ $menuItem->name }}
+                                    @if($menuItem->children && $menuItem->children->count() > 0)
+                                        (有子菜单)
+                                    @endif
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    {{-- 排序 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">排序</label>
+                        <input type="number"
+                               name="sort_order"
+                               class="control"
+                               value="{{ old('sort_order', 0) }}"
+                               min="0"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            数字越小越靠前
+                        </small>
+                    </div>
+
+                    {{-- 状态 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">状态</label>
+                        <div>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="1" {{ old('status', 1) == 1 ? 'checked' : '' }} style="margin-right: 5px;">
+                                启用
+                            </label>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="0" {{ old('status') == 0 ? 'checked' : '' }} style="margin-right: 5px;">
+                                禁用
+                            </label>
+                        </div>
+                    </div>
+
+                    {{-- 提交按钮 --}}
+                    <div class="form-group" style="margin-top: 30px;">
+                        <button type="submit" class="btn btn-primary" style="padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 18px; cursor: pointer; margin-right: 10px;">
+                            <i class="fas fa-save"></i> 保存菜单项
+                        </button>
+                        <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 12px 24px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; font-size: 18px;">
+                            <i class="fas fa-times"></i> 取消
+                        </a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+    {{-- 引入必要的JavaScript --}}
+@push('scripts')
+<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+<script>
+   /* $(document).ready(function() {
+        // 自动生成key
+        $('input[name="name"]').on('blur', function() {
+            var name = $(this).val();
+            var keyInput = $('input[name="key"]');
+
+            if (keyInput.val() === '') {
+                // 将名称转换为小写,空格替换为横线,移除特殊字符
+                var key = name.toLowerCase()
+                             .replace(/\s+/g, '-')
+                             .replace(/[^a-z0-9-]/g, '');
+                // 默认添加到 settings 下
+                keyInput.val('settings.' + key);
+
+                // 添加一个视觉提示
+                keyInput.css('border-color', '#28a745');
+                setTimeout(function() {
+                    keyInput.css('border-color', '');
+                }, 1000);
+
+                // 显示成功提示
+                var successMsg = $('<small class="key-success" style="display: block; margin-top: 5px; font-size: 12px; color: #28a745;">' +
+                                 '✓ 已自动生成Key: settings.' + key + '</small>');
+                keyInput.after(successMsg);
+                setTimeout(function() {
+                    $('.key-success').remove();
+                }, 3000);
+            }
+        });
+
+        // 为所有表单元素添加Bagisto样式类
+        $('.control').addClass('form-control');
+
+        // 检查key格式
+        $('form').on('submit', function(e) {
+            var keyValue = $('input[name="key"]').val();
+            if (keyValue && keyValue.indexOf('settings.') !== 0) {
+                if (!confirm('当前Key不以"settings."开头,菜单可能不会显示在Settings下。确定要继续吗?')) {
+                    e.preventDefault();
+                }
+            }
+        });
+
+        // 添加提示
+        $('input[name="key"]').on('focus', function() {
+            var currentVal = $(this).val();
+            if (currentVal && currentVal.indexOf('settings.') !== 0) {
+                $(this).after(
+                    '<small class="key-warning" style="display: block; margin-top: 5px; font-size: 12px; color: #dc3545;">' +
+                    '⚠️ 警告:当前Key不以"settings."开头,菜单可能不会显示在Settings下</small>'
+                );
+
+                setTimeout(function() {
+                    $('.key-warning').remove();
+                }, 3000);
+            }
+        });
+    });*/
+</script>
+@endpush
+</x-admin::layouts>

+ 179 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/edit.blade.php

@@ -0,0 +1,179 @@
+<x-admin::layouts>
+    {{-- 页面标题 --}}
+    <x-slot:title>编辑菜单项 - {{ $menuItem->name }}</x-slot:title>
+
+    {{-- 页面内容 --}}
+    <div class="content" style="padding: 20px;">
+        {{-- 页头部分 --}}
+        <div class="page-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #dee2e6;">
+            <div class="page-title">
+                <h1><i class="fas fa-edit"></i> 编辑菜单项: {{ $menuItem->name }}</h1>
+            </div>
+            <div class="page-action">
+                <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 8px 16px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;">
+                    <i class="fas fa-arrow-left"></i> 返回列表
+                </a>
+            </div>
+        </div>
+
+        {{-- 显示错误信息 --}}
+        @if($errors->any())
+            <div class="alert alert-danger" style="background: #f8d7da; color: #721c24; padding: 12px; border-radius: 4px; margin-bottom: 20px; border: 1px solid #f5c6cb;">
+                <ul class="mb-0" style="margin: 0; padding-left: 20px;">
+                    @foreach($errors->all() as $error)
+                        <li>{{ $error }}</li>
+                    @endforeach
+                </ul>
+            </div>
+        @endif
+
+        {{-- 表单面板 --}}
+        <div class="panel" style="background: white; border: 1px solid #dee2e6; border-radius: 4px; margin-bottom: 20px;">
+            <div class="panel-header" style="padding: 12px 20px; background: #f8f9fa; border-bottom: 1px solid #dee2e6;">
+                <h3 style="margin: 0; font-size: 18px;">编辑基本信息</h3>
+            </div>
+
+            <div class="panel-body" style="padding: 20px;">
+                <form method="POST" action="{{ route('admin.dynamicmenu.update', $menuItem->id) }}">
+                    @csrf
+                    @method('PUT')
+
+                    {{-- 名称 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label class="required" style="display: block; margin-bottom: 5px; font-weight: 500;">
+                            名称 <span style="color: red;">*</span>
+                        </label>
+                        <input type="text"
+                               name="name"
+                               class="control"
+                               value="{{ old('name', $menuItem->name) }}"
+                               required
+                               placeholder="例如:仪表盘"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                    </div>
+
+                    {{-- Key 字段 --}}
+                    <div class="form-group">
+                        <label class="required">Key</label>
+                        <input type="text"
+                            name="key"
+                            class="control"
+                            value="{{ old('key', $menuItem->key ?? '') }}"
+                            required
+                            placeholder="例如:settings.my-menu">
+                        <small class="control-info">
+                            建议以 'settings.' 开头,这样菜单会显示在 Settings 下。例如:settings.dynamic-menu
+                        </small>
+                    </div>
+
+                    {{-- URL/路由 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">URL/路由</label>
+                        <input type="text"
+                               name="route"
+                               class="control"
+                               value="{{ old('route', $menuItem->route) }}"
+                               placeholder="例如:admin.dashboard"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            可以是相对路径、完整URL或路由名称
+                        </small>
+                    </div>
+
+                    {{-- 图标 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">图标</label>
+                        <input type="text"
+                               name="icon"
+                               class="control"
+                               value="{{ old('icon', $menuItem->icon) }}"
+                               placeholder="例如:fas fa-dashboard、fas fa-users等"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            FontAwesome图标类名,如:fas fa-home
+                        </small>
+                    </div>
+
+                    {{-- 父级菜单 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">父级菜单</label>
+                        <select name="parent_id" class="control" style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                            <option value="">作为顶级菜单</option>
+                            @foreach($menuItems as $menuItemOption)
+                                @if($menuItemOption->id != $menuItem->id) {{-- 不能选择自己作为父级 --}}
+                                <option value="{{ $menuItemOption->id }}" {{ old('parent_id', $menuItem->parent_id) == $menuItemOption->id ? 'selected' : '' }}>
+                                    {{ $menuItemOption->name }}
+                                    @if($menuItemOption->children && $menuItemOption->children->count() > 0)
+                                        (有子菜单)
+                                    @endif
+                                </option>
+                                @endif
+                            @endforeach
+                        </select>
+                    </div>
+
+                    {{-- 排序 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">排序</label>
+                        <input type="number"
+                               name="sort_order"
+                               class="control"
+                               value="{{ old('sort_order', $menuItem->sort_order) }}"
+                               min="0"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            数字越小越靠前
+                        </small>
+                    </div>
+
+                    {{-- 状态 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">状态</label>
+                        <div>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="1" {{ old('status', $menuItem->status) == 1 ? 'checked' : '' }} style="margin-right: 5px;">
+                                启用
+                            </label>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="0" {{ old('status', $menuItem->status) == 0 ? 'checked' : '' }} style="margin-right: 5px;">
+                                禁用
+                            </label>
+                        </div>
+                    </div>
+
+                    {{-- 提交按钮 --}}
+                    <div class="form-group" style="margin-top: 30px;">
+                        <button type="submit" class="btn btn-primary" style="padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 18px; cursor: pointer; margin-right: 10px;">
+                            <i class="fas fa-save"></i> 更新菜单项
+                        </button>
+                        <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 12px 24px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; font-size: 18px;">
+                            <i class="fas fa-times"></i> 取消
+                        </a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+    {{-- 引入必要的JavaScript --}}
+    @push('scripts')
+    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+    <script>
+        /*$(document).ready(function() {
+            // 自动生成key
+            $('input[name="name"]').on('blur', function() {
+                var name = $(this).val();
+                var keyInput = $('input[name="key"]');
+
+                if (keyInput.val() === '') {
+                    var key = name.toLowerCase()
+                                .replace(/\s+/g, '-')
+                                .replace(/[^a-z0-9-]/g, '');
+                    // 默认添加到 settings 下
+                    keyInput.val('settings.' + key);
+                }
+            });
+        });*/
+    </script>
+    @endpush
+</x-admin::layouts>

+ 99 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/index.blade.php

@@ -0,0 +1,99 @@
+<x-admin::layouts>
+    {{-- 页面标题 --}}
+    <x-slot:title>
+        @lang('dynamicmenu::app.admin.menu.title')
+    </x-slot>
+
+    <div class="content">
+        {{-- 页头部分 --}}
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="py-[11px] text-xl text-gray-800 dark:text-white font-bold">
+                @lang('dynamicmenu::app.admin.menu.title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <a href="{{ route('admin.dynamicmenu.create') }}" class="primary-button">
+                    @lang('dynamicmenu::app.admin.menu.add-btn')
+                </a>
+            </div>
+        </div>
+
+        {{-- 页面内容 --}}
+        <div class="mt-8">
+            @if(session('success'))
+                <div class="alert alert-success">
+                    {{ session('success') }}
+                </div>
+            @endif
+
+            @if(session('error'))
+                <div class="alert alert-danger">
+                    {{ session('error') }}
+                </div>
+            @endif
+
+            <div class="accordion">
+                @foreach($menuItems as $item)
+                    @include('dynamicmenu::admin.menu.partials.tree', ['item' => $item])
+                @endforeach
+            </div>
+        </div>
+    </div>
+
+    {{-- 自定义样式 --}}
+    @push('styles')
+    <style>
+        /* 将您原有的 <style> 标签内容移到这里 */
+        .accordion { margin: 10px 0; }
+        .accordion-item { margin: 5px 0; border: 1px solid #dee2e6; border-radius: 4px; overflow: hidden; }
+        .accordion-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: #f8f9fa; cursor: pointer; transition: background 0.3s; }
+        .accordion-header:hover { background: #e9ecef; }
+        .accordion-title { display: flex; align-items: center; gap: 8px; }
+        .accordion-title i { font-size: 16px; color: #6c757d; }
+        .accordion-title .text-muted { color: #6c757d; font-size: 0.9em; }
+        .accordion-actions { display: flex; gap: 8px; }
+        .btn-edit, .btn-delete { padding: 4px 8px; border: none; background: none; cursor: pointer; border-radius: 4px; transition: background 0.3s; }
+        .btn-edit { color: #0d6efd; }
+        .btn-edit:hover { background: #e7f1ff; }
+        .btn-delete { color: #dc3545; }
+        .btn-delete:hover:not(.disabled) { background: #f8d7da; }
+        .btn-delete.disabled { color: #6c757d; cursor: not-allowed; opacity: 0.5; }
+        .accordion-content { display: none; padding: 15px 15px 15px 30px; background: white; border-top: 1px solid #dee2e6; }
+        .accordion-content.expanded { display: block; }
+        .accordion-header::after { content: '▶'; font-size: 12px; transition: transform 0.3s; margin-left: 10px; }
+        .accordion-header.expanded::after { transform: rotate(90deg); }
+    </style>
+    @endpush
+
+    {{-- 自定义脚本 --}}
+    @push('scripts')
+    <script>
+        function toggleAccordion(header) {
+            if (event.target.closest('.accordion-actions')) {
+                return;
+            }
+
+            header.classList.toggle('expanded');
+            var content = header.nextElementSibling;
+
+            if (content && content.classList.contains('accordion-content')) {
+                content.classList.toggle('expanded');
+            }
+        }
+
+        // 页面加载完成后,可以默认展开所有有子菜单的项(可选)
+        document.addEventListener('DOMContentLoaded', function() {
+            // 如果你想默认展开所有菜单,取消下面的注释
+            /*
+            document.querySelectorAll('.accordion-header').forEach(function(header) {
+                var content = header.nextElementSibling;
+                if (content && content.classList.contains('accordion-content')) {
+                    header.classList.add('expanded');
+                    content.classList.add('expanded');
+                }
+            });
+            */
+        });
+    </script>
+    @endpush
+</x-admin::layouts>

+ 44 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/partials/tree.blade.php

@@ -0,0 +1,44 @@
+<div class="accordion-item">
+    <div class="accordion-header {{ $item->children && $item->children->count() > 0 ? 'has-children' : '' }}" 
+         onclick="toggleAccordion(this)">
+        <div class="accordion-title">
+            <i class="{{ $item->icon ?? 'icon-file' }}"></i>
+            <span>{{ $item->name }}</span>
+            <span class="text-muted">({{ $item->key ?? $item->url ?? '无标识' }})</span>
+            @if(!$item->status)
+                <span class="badge badge-warning" style="background: #ffc107; color: #000; padding: 2px 6px; border-radius: 4px; font-size: 0.8em;">已禁用</span>
+            @endif
+        </div>
+        <div class="accordion-actions" onclick="event.stopPropagation()">
+            {{-- 编辑按钮 --}}
+            <a href="{{ route('admin.dynamicmenu.edit', $item->id) }}" class="btn-edit" title="编辑">
+                <i class="fas fa-edit"></i> 编辑
+            </a>
+            
+            {{-- 删除按钮 --}}
+            @if(!$item->children || $item->children->count() == 0)
+                <form action="{{ route('admin.dynamicmenu.delete', $item->id) }}" method="POST" style="display: inline;" onsubmit="return confirm('确定删除这个菜单项吗?');">
+                    @csrf
+                    @method('DELETE')
+                    <button type="submit" class="btn-delete" title="删除">
+                        <i class="fas fa-trash"></i> 删除
+                    </button>
+                </form>
+            @else
+                <span class="btn-delete disabled" title="请先删除子菜单">
+                    <i class="fas fa-trash"></i> 删除
+                </span>
+            @endif
+        </div>
+    </div>
+    
+    @if($item->children && $item->children->count() > 0)
+        <div class="accordion-content">
+            <div class="accordion">
+                @foreach($item->children as $child)
+                    @include('dynamicmenu::admin.menu.partials.tree', ['item' => $child])
+                @endforeach
+            </div>
+        </div>
+    @endif
+</div>

+ 22 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/permission/index.blade.php

@@ -0,0 +1,22 @@
+@extends('admin::layouts.master')
+
+@section('page_title')
+    菜单权限管理
+@stop
+
+@section('content-wrapper')
+    <div class="content full-page">
+        <div class="page-header">
+            <div class="page-title">
+                <h1>菜单权限管理</h1>
+            </div>
+        </div>
+        <div class="page-content">
+            <div class="panel">
+                <div class="panel-body">
+                    <p>权限管理页面</p>
+                </div>
+            </div>
+        </div>
+    </div>
+@stop

+ 57 - 0
packages/Longyi/DynamicMenu/src/Routes/admin-routes.php

@@ -0,0 +1,57 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\DynamicMenu\Http\Controllers\MenuController;
+
+// 关键:使用正确的路由组
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin'], function () {
+    
+    
+    
+    // 菜单管理路由 - 注意这里不要重复加 'admin' 前缀
+    Route::get('dynamicmenu', [MenuController::class, 'index'])
+        ->name('admin.dynamicmenu.index')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.menu.index'
+        ]);
+    
+    Route::get('dynamicmenu/create', [MenuController::class, 'create'])
+        ->name('admin.dynamicmenu.create')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.menu.create'
+        ]);
+    
+    Route::post('dynamicmenu', [MenuController::class, 'store'])
+        ->name('admin.dynamicmenu.store')
+        ->defaults('_config', [
+            'redirect' => 'admin.dynamicmenu.index'
+        ]);
+    
+    Route::get('dynamicmenu/{id}/edit', [MenuController::class, 'edit'])
+        ->name('admin.dynamicmenu.edit')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.menu.edit'
+        ]);
+    
+    Route::put('dynamicmenu/{id}', [MenuController::class, 'update'])
+        ->name('admin.dynamicmenu.update')
+        ->defaults('_config', [
+            'redirect' => 'admin.dynamicmenu.index'
+        ]);
+    
+    Route::delete('dynamicmenu/{id}', [MenuController::class, 'destroy'])
+        ->name('admin.dynamicmenu.delete');
+    
+    // 权限管理路由
+    Route::get('dynamicmenu-permissions', [MenuController::class, 'permission'])
+        ->name('admin.dynamicmenu.permission')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.permission.index'
+        ]);
+    
+    Route::post('dynamicmenu-permissions', [MenuController::class, 'updatePermission'])
+        ->name('admin.dynamicmenu.permission.update');
+    
+    Route::post('dynamicmenu-permissions/get', [MenuController::class, 'getRolePermissions'])
+        ->name('admin.dynamicmenu.permission.get');
+});

+ 49 - 0
packages/Longyi/DynamicMenu/src/Services/MenuService.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Longyi\DynamicMenu\Services;
+
+use Longyi\DynamicMenu\Models\MenuItem;
+use Illuminate\Support\Facades\Cache;
+
+class MenuService
+{
+
+    public function boot()
+    {
+        // ... 其他代码
+
+        Event::listen('bagisto.admin.menu.build', function ($menu) {
+            $settings = $menu->get('settings');
+            
+            if ($settings) {
+                // 使用缓存,避免每次请求都查询数据库
+                $menuItems = Cache::remember('dynamic_menu_items', 3600, function () {
+                    return app(MenuService::class)->getEnabledMenuItems();
+                });
+                
+                foreach ($menuItems as $item) {
+                    $this->addMenuItemToMenu($settings, $item);
+                }
+            }
+        });
+    }
+    /**
+     * 获取所有启用的菜单项
+     */
+    public function getEnabledMenuItems()
+    {
+        return MenuItem::with('children')
+            ->whereNull('parent_id')
+            ->where('status', 1)
+            ->orderBy('sort_order')
+            ->get();
+    }
+    
+    /**
+     * 清除菜单缓存
+     */
+    public function clearCache()
+    {
+        Cache::forget('dynamic_menu_items');
+    }
+}

+ 64 - 0
packages/Longyi/RewardPoints/README.md

@@ -0,0 +1,64 @@
+### 依赖关系
+
+- **PHP**: ^8.1|^8.2
+- **Laravel**: ^10.0|^11.0
+- **Bagisto**: 2.x
+
+---
+
+## 安装步骤
+
+### 1. 确认插件已注册
+
+确保 `bootstrap/providers.php` 文件中已添加:
+
+Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
+
+### 2. 运行数据库迁移
+php artisan migrate
+这将创建以下 5 个数据表:
+- `mw_reward_active_rules` - 积分活动规则表
+- `mw_reward_point_customer` - 客户积分表
+- `mw_reward_point_customer_sign` - 客户签到表
+- `mw_reward_point_history` - 积分历史表
+- `mw_reward_points_settings` - 积分设置表
+
+### 3. 初始化默认设置
+php artisan reward-points:init-settings
+### 4. 清理缓存
+php artisan cache:clear      # 清除应用缓存
+php artisan config:clear     # 清除配置缓存
+php artisan view:clear       # 清除视图缓存
+php artisan route:clear      # 清除路由缓存
+## 定时任务
+php artisan reward-points:check-expired
+### 配置 Cron
+在服务器的 crontab 中添加:
+每天凌晨 2 点检查过期积分
+0 2 * * * cd /path/to/bagisto && php artisan reward-points:check-expired
+或在 Laravel Scheduler 中配置(`app/Console/Kernel.php`):
+protected function schedule(Schedule $schedule): void { 
+    $schedule->command('reward-points:check-expired')
+    ->dailyAt('02:00'); 
+}
+
+## 版本历史
+
+### v1.0.0 (2026-01-01)
+- ✨ 初始版本发布
+- ✅ 完整的积分管理系统
+- ✅ 多种积分获取方式
+- ✅ 积分抵扣功能
+- ✅ 积分有效期管理
+- ✅ 命令行初始化工具
+- ✅ 定时任务支持
+
+---
+
+## 技术支持
+
+如有问题或建议,请联系开发团队。
+
+**开发者**: Longyi Team  
+**邮箱**: dev@longyi.com  
+**许可证**: MIT License

+ 31 - 0
packages/Longyi/RewardPoints/composer.json

@@ -0,0 +1,31 @@
+{
+    "name": "longyi/reward-points",
+    "description": "Reward Points System for Bagisto",
+    "keywords": ["bagisto", "reward", "points", "loyalty"],
+    "license": "MIT",
+    "type": "library",
+    "version": "1.0.0",
+    "authors": [
+        {
+            "name": "Longyi Team",
+            "email": "dev@longyi.com"
+        }
+    ],
+    "require": {
+        "php": "^8.1|^8.2",
+        "illuminate/support": "^10.0|^11.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Longyi\\RewardPoints\\": "src/"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider"
+            ],
+            "aliases": {}
+        }
+    }
+}

+ 43 - 0
packages/Longyi/RewardPoints/src/Config/rewardpoints.php

@@ -0,0 +1,43 @@
+<?php
+
+return [
+    'general' => [
+        'point_value' => 0.01,
+        'max_discount_percentage' => 100,
+        'min_points_to_redeem' => 100,
+        'enabled' => true,
+    ],
+
+    'expiration' => [
+        'default_days' => 365,
+        'enable_notification' => true,
+        'notification_days_before' => 7,
+    ],
+
+    'sign_in' => [
+        'base_points' => 10,
+        'week_bonus_points' => 20,
+        'month_bonus_points' => 100,
+    ],
+
+    'order' => [
+        'points_per_currency' => 10,
+        'min_order_amount' => 0,
+        'exclude_shipping' => true,
+        'exclude_tax' => false,
+    ],
+
+    'review' => [
+        'points_per_review' => 50,
+        'require_approval' => true,
+    ],
+
+    'registration' => [
+        'points_per_registration' => 100,
+    ],
+
+    'referral' => [
+        'points_per_referral' => 200,
+        'require_first_order' => true,
+    ],
+];

+ 43 - 0
packages/Longyi/RewardPoints/src/Console/Commands/CheckExpiredPoints.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace Longyi\RewardPoints\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Carbon\Carbon;
+
+class CheckExpiredPoints extends Command
+{
+    protected $signature = 'reward-points:check-expired';
+
+    protected $description = 'Check and process expired reward points';
+
+    protected $rewardPointRepository;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        parent::__construct();
+        $this->rewardPointRepository = $rewardPointRepository;
+    }
+
+    public function handle()
+    {
+        $this->info('Checking expired reward points...');
+        
+        $startTime = Carbon::now();
+        
+        try {
+            $this->rewardPointRepository->checkExpiredPoints();
+            
+            $endTime = Carbon::now();
+            $duration = $startTime->diffInSeconds($endTime);
+            
+            $this->info("Successfully processed expired points. Duration: {$duration} seconds");
+            return 0;
+            
+        } catch (\Exception $e) {
+            $this->error('Error processing expired points: ' . $e->getMessage());
+            return 1;
+        }
+    }
+}

+ 117 - 0
packages/Longyi/RewardPoints/src/Console/Commands/InitializeSettings.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace Longyi\RewardPoints\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\RewardPoints\Repositories\RewardPointSettingRepository;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class InitializeSettings extends Command
+{
+    protected $signature = 'reward-points:init-settings';
+
+    protected $description = 'Initialize reward points settings in database';
+
+    protected $settingRepository;
+
+    public function __construct(RewardPointSettingRepository $settingRepository)
+    {
+        parent::__construct();
+        $this->settingRepository = $settingRepository;
+    }
+
+    public function handle()
+    {
+        $this->info('Initializing reward points settings...');
+
+        try {
+            $this->settingRepository->initializeDefaultSettings();
+
+            $this->info('✓ Default settings initialized successfully!');
+            $this->info('You can now configure reward points in admin panel.');
+
+            // 添加积分模块菜单项到动态菜单
+            $this->addMenuItems();
+
+            return 0;
+
+        } catch (\Exception $e) {
+            $this->error('Error initializing settings: ' . $e->getMessage());
+            return 1;
+        }
+    }
+
+    /**
+     * 添加积分模块的菜单项到动态菜单
+     */
+    protected function addMenuItems()
+    {
+        $this->info('Adding reward points menu items to dynamic menu...');
+
+        // 检查是否已存在积分模块的菜单项
+        $existingParent = MenuItem::where('key', 'settings.reward-points')->first();
+
+        if ($existingParent) {
+            $this->warn('Reward points menu items already exist in dynamic menu.');
+            return;
+        }
+
+        // 创建父级菜单项
+        $parentItem = MenuItem::create([
+            'name' => '积分管理',
+            'key' => 'rewardPoint',
+            'route' => 'admin.reward-points.rules.index',
+            'icon' => 'icon-star',
+            'sort_order' => 10,
+            'status' => 1,
+            'created_by' => 1,
+        ]);
+
+        // 创建子菜单项
+        $childItems = [
+            [
+                'name' => '积分规则',
+                'key' => 'rewardPoint.rules',
+                'route' => 'admin.reward-points.rules.index',
+                'icon' => 'icon-list',
+                'sort_order' => 1,
+            ],
+            [
+                'name' => '客户积分',
+                'key' => 'rewardPoint.customers',
+                'route' => 'admin.reward-points.customers.index',
+                'icon' => 'icon-users',
+                'sort_order' => 2,
+            ],
+            [
+                'name' => '积分记录',
+                'key' => 'rewardPoint.reports',
+                'route' => 'admin.reward-points.transactions.index',
+                'icon' => 'icon-chart',
+                'sort_order' => 3,
+            ],
+            [
+                'name' => '积分设置',
+                'key' => 'rewardPoint.settings',
+                'route' => 'admin.reward-points.settings.index',
+                'icon' => 'icon-setting',
+                'sort_order' => 4,
+            ],
+        ];
+
+        foreach ($childItems as $item) {
+            MenuItem::create([
+                'name' => $item['name'],
+                'key' => $item['key'],
+                'route' => $item['route'],
+                'icon' => $item['icon'],
+                'sort_order' => $item['sort_order'],
+                'parent_id' => $parentItem->id,
+                'status' => 1,
+                'created_by' => 1,
+            ]);
+        }
+
+        $this->info('✓ Reward points menu items added to dynamic menu successfully!');
+    }
+}

+ 116 - 0
packages/Longyi/RewardPoints/src/Database/Migrations/2026_01_01_000001_create_points_tables.php

@@ -0,0 +1,116 @@
+<?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
+    {
+        if (!Schema::hasTable('mw_reward_active_rules')) {
+            Schema::create('mw_reward_active_rules', function (Blueprint $table) {
+                $table->increments('rule_id');
+                $table->string('rule_name', 255)->default('');
+                $table->integer('type_of_transaction')->default(0);
+                $table->string('store_view', 255)->default('0');
+                $table->string('enable_different_points_by_group', 255)->default('');
+                $table->string('customer_group_ids', 255)->default(false);
+                $table->integer('default_expired')->nullable()->default(1);
+                $table->integer('expired_day')->nullable()->default(0);
+                $table->string('date_event', 255)->default('');
+                $table->string('comment', 255)->default('');
+                $table->string('coupon_code', 255)->nullable()->default('');
+                $table->string('reward_point', 255)->default('0');
+                $table->integer('status')->default(0);
+            });
+        }
+
+        if (!Schema::hasTable('mw_reward_point_customer')) {
+            Schema::create('mw_reward_point_customer', function (Blueprint $table) {
+                $table->increments('id');
+                $table->unsignedInteger('customer_id');
+                $table->unsignedBigInteger('mw_reward_point')->default(0);
+                $table->unsignedInteger('mw_friend_id')->default(0);
+                $table->boolean('subscribed_balance_update')->default(true);
+                $table->boolean('subscribed_point_expiration')->default(true);
+                $table->timestamp('last_checkout')->useCurrent();
+                
+                $table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade');
+            });
+        }
+
+        if (!Schema::hasTable('mw_reward_point_customer_sign')) {
+            Schema::create('mw_reward_point_customer_sign', function (Blueprint $table) {
+                $table->increments('id');
+                $table->unsignedInteger('customer_id');
+                $table->string('sign_date', 255)->default('');
+                $table->unsignedInteger('count_date');
+                $table->unsignedInteger('point');
+                $table->string('code', 255)->default('');
+                $table->dateTime('created')->nullable();
+                $table->dateTime('updated')->nullable();
+                
+                $table->index('customer_id', 'customer_id_index');
+            });
+        }
+
+        if (!Schema::hasTable('mw_reward_point_history')) {
+            Schema::create('mw_reward_point_history', function (Blueprint $table) {
+                $table->increments('history_id');
+                $table->unsignedInteger('customer_id');
+                $table->unsignedTinyInteger('type_of_transaction');
+                $table->integer('amount');
+                $table->integer('balance');
+                $table->text('transaction_detail')->nullable();
+                $table->timestamp('transaction_time')->useCurrent();
+                $table->unsignedInteger('history_order_id')->nullable()->default(0);
+                $table->unsignedSmallInteger('expired_day')->default(0);
+                $table->timestamp('expired_time')->nullable();
+                $table->integer('point_remaining')->default(0);
+                $table->boolean('check_time')->default(true);
+                $table->tinyInteger('status')->default(0);
+                
+                $table->index('customer_id', 'customer_id_index');
+                $table->index('transaction_time', 'transaction_time_index');
+                $table->index(['customer_id', 'status'], 'customer_status_index');
+                $table->index(['customer_id', 'history_order_id'], 'customer_order_index');
+                $table->index('expired_time', 'expired_time_index');
+                
+                $table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade');
+            });
+        }
+        if (!Schema::hasTable('mw_reward_points_settings')) {
+            Schema::create('mw_reward_points_settings', function (Blueprint $table) {
+                $table->increments('id');
+                $table->string('code')->unique()->comment('配置代码');
+                $table->string('name')->comment('配置名称');
+                $table->text('value')->nullable()->comment('配置值');
+                $table->string('type')->default('text')->comment('配置类型:text, number, boolean, select');
+                $table->string('group')->default('general')->comment('配置分组');
+                $table->integer('sort_order')->default(0)->comment('排序');
+                $table->text('options')->nullable()->comment('选项(JSON 格式,用于 select 类型)');
+                $table->string('description')->nullable()->comment('描述');
+                $table->boolean('is_enabled')->default(true)->comment('是否启用');
+                $table->timestamps();
+                
+                $table->index('group', 'idx_group');
+                $table->index('is_enabled', 'idx_enabled');
+            });
+        }
+    }
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('mw_reward_point_history');
+        Schema::dropIfExists('mw_reward_point_customer_sign');
+        Schema::dropIfExists('mw_reward_point_customer');
+        Schema::dropIfExists('mw_reward_active_rules');
+        Schema::dropIfExists('mw_reward_points_settings');
+    }
+};

+ 18 - 0
packages/Longyi/RewardPoints/src/Facades/CartRewardPoints.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Longyi\RewardPoints\Facades;
+
+use Illuminate\Support\Facades\Facade;
+
+class CartRewardPoints extends Facade
+{
+    /**
+     * Get the registered name of the component.
+     *
+     * @return string
+     */
+    protected static function getFacadeAccessor()
+    {
+        return 'cartrewardpoints';
+    }
+}

+ 282 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/CustomerController.php

@@ -0,0 +1,282 @@
+<?php
+
+namespace Longyi\RewardPoints\Http\Controllers\Admin;
+
+use Illuminate\Http\Request;
+use Webkul\Admin\Http\Controllers\Controller;
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Models\RewardPointCustomer;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+use Webkul\Customer\Models\Customer;
+use Carbon\Carbon;
+
+class CustomerController extends Controller
+{
+    protected $rewardPointRepository;
+    protected $_config;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        $this->rewardPointRepository = $rewardPointRepository;
+        $this->_config = request('_config') ?: [];
+    }
+
+    /**
+     * Display a listing of customers with reward points
+     */
+    public function index()
+    {
+        $query = RewardPointCustomer::with('customer');
+
+        // 邮箱筛选
+        if ($email = request('email')) {
+            $query->whereHas('customer', function ($q) use ($email) {
+                $q->where('email', 'like', "%{$email}%");
+            });
+        }
+
+        // 积分最小值筛选
+        if ($minPoints = request('min_points')) {
+            $query->where('mw_reward_point', '>=', $minPoints);
+        }
+
+        // 积分最大值筛选
+        if ($maxPoints = request('max_points')) {
+            $query->where('mw_reward_point', '<=', $maxPoints);
+        }
+
+        $customers = $query->orderBy('mw_reward_point', 'desc')->paginate(15);
+
+        $totalPoints = RewardPointCustomer::sum('mw_reward_point');
+        $totalCustomers = RewardPointCustomer::count();
+        $averagePoints = $totalCustomers > 0 ? $totalPoints / $totalCustomers : 0;
+
+        $view = isset($this->_config['view']) ? $this->_config['view'] : 'rewardpoints::admin.customers.index';
+        return view($view, compact('customers', 'totalPoints', 'totalCustomers', 'averagePoints'));
+    }
+
+    /**
+     * Show customer reward points details
+     */
+    public function show($customerId)
+    {
+        $customer = Customer::findOrFail($customerId);
+        $rewardData = RewardPointCustomer::where('customer_id', $customerId)->first();
+
+        if (!$rewardData) {
+            $rewardData = new RewardPointCustomer();
+            $rewardData->customer_id = $customerId;
+            $rewardData->mw_reward_point = 0;
+            $rewardData->mw_friend_id = 0;
+            $rewardData->last_checkout = Carbon::now();
+        }
+
+        $history = $this->rewardPointRepository->getHistory($customerId, 20);
+        $signRecords = $rewardData->signRecords()->orderBy('sign_date', 'desc')->limit(30)->get();
+
+        return view($this->_config['view'], compact('customer', 'rewardData', 'history', 'signRecords'));
+    }
+
+    public function adjustPointsForm()
+    {
+
+        $view = isset($this->_config['view']) ? $this->_config['view'] : 'rewardpoints::admin.customers.adjust-points';
+        return view($view);
+    }
+
+     /**
+     * Adjust points by email or ID
+     */
+    public function adjustPointsByIdOrEmail(Request $request)
+    {
+        $this->validate($request, [
+            'identifier' => 'required|string',
+            'action' => 'required|in:add,deduct',
+            'points' => 'required|integer|min:1',
+            'reason' => 'nullable|string|max:500'
+        ]);
+
+        try {
+            $identifier = trim($request->input('identifier'));
+            $action = $request->input('action');
+            $points = $request->input('points');
+            $reason = $request->input('reason', 'Admin adjustment');
+
+            // 首先尝试按邮箱查找用户
+            $customer = Customer::where('email', $identifier)->first();
+
+            // 如果没找到邮箱,尝试按ID查找
+            if (!$customer) {
+                $customerId = (int)$identifier;
+                if (is_numeric($identifier)) {
+                    $customer = Customer::find($customerId);
+                }
+            }
+
+            if (!$customer) {
+                session()->flash('error', 'Customer not found with provided email or ID: ' . $identifier);
+                return redirect()->back()->withInput();
+            }
+
+            // 调用现有的积分调整方法
+            if ($action === 'add') {
+                $result = $this->rewardPointRepository->addPoints(
+                    $customer->id,
+                    99, // 99 for admin action
+                    $points,
+                    null,
+                    "Admin: " . $reason
+                );
+
+                if ($result) {
+                    session()->flash('success', "Successfully added {$points} points to customer {$customer->email}.");
+                } else {
+                    session()->flash('error', 'Failed to add points to customer.');
+                }
+            } else {
+                $result = $this->rewardPointRepository->deductPoints(
+                    $customer->id,
+                    $points,
+                    null,
+                    "Admin: " . $reason
+                );
+
+                if ($result) {
+                    session()->flash('success', "Successfully deducted {$points} points from customer {$customer->email}.");
+                } else {
+                    session()->flash('error', 'Insufficient points or customer not found.');
+                }
+            }
+
+            return redirect()->route('admin.reward-points.customers.index');
+
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error adjusting points: ' . $e->getMessage());
+            return redirect()->back()->withInput();
+        }
+    }
+
+    /**
+     * Bulk update points
+     */
+    public function bulkUpdate(Request $request)
+    {
+        $this->validate($request, [
+            'customer_ids' => 'required|array',
+            'customer_ids.*' => 'integer|exists:customers,id',
+            'points' => 'required|integer',
+            'action' => 'required|in:add,set'
+        ]);
+
+        try {
+            $customerIds = $request->input('customer_ids');
+            $points = $request->input('points');
+            $action = $request->input('action');
+            $reason = $request->input('reason', 'Admin bulk update');
+
+            $updatedCount = 0;
+
+            foreach ($customerIds as $customerId) {
+                if ($action === 'add') {
+                    $result = $this->rewardPointRepository->addPoints(
+                        $customerId,
+                        99, // 99 for admin action
+                        $points,
+                        null,
+                        "Admin bulk: " . $reason
+                    );
+                } else {
+                    // Set points to specific value
+                    $customerPoints = RewardPointCustomer::firstOrCreate(
+                        ['customer_id' => $customerId],
+                        [
+                            'mw_reward_point' => 0,
+                            'mw_friend_id' => 0,
+                            'last_checkout' => Carbon::now()
+                        ]
+                    );
+
+                    $oldPoints = $customerPoints->mw_reward_point;
+                    $customerPoints->mw_reward_point = $points;
+                    $customerPoints->save();
+
+                    // Add history record
+                    $this->rewardPointRepository->create([
+                        'customer_id' => $customerId,
+                        'type_of_transaction' => 0,
+                        'amount' => $points - $oldPoints,
+                        'balance' => $points,
+                        'transaction_detail' => "Admin bulk set: " . $reason,
+                        'transaction_time' => Carbon::now(),
+                        'history_order_id' => 0,
+                        'expired_day' => 0,
+                        'expired_time' => null,
+                        'point_remaining' => 0,
+                        'check_time' => 1,
+                        'status' => 1
+                    ]);
+
+                    $result = true;
+                }
+
+                if ($result) {
+                    $updatedCount++;
+                }
+            }
+
+            session()->flash('success', "Successfully updated {$updatedCount} customers.");
+            return redirect()->back();
+
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error in bulk update: ' . $e->getMessage());
+            return redirect()->back()->withInput();
+        }
+    }
+
+    /**
+     * Export customer points report
+     */
+    public function export(Request $request)
+    {
+        $customers = RewardPointCustomer::with('customer')
+            ->orderBy('mw_reward_point', 'desc')
+            ->get();
+
+        $filename = "reward_points_report_" . Carbon::now()->format('Y-m-d_H-i-s') . ".csv";
+
+        $headers = [
+            'Content-Type' => 'text/csv',
+            'Content-Disposition' => "attachment; filename={$filename}",
+        ];
+
+        $callback = function() use ($customers) {
+            $file = fopen('php://output', 'w');
+
+            // Add CSV headers
+            fputcsv($file, [
+                'Customer ID',
+                'Customer Email',
+                'Customer Name',
+                'Reward Points',
+                'Last Checkout'
+            ]);
+
+            // Add data rows
+            foreach ($customers as $customer) {
+                fputcsv($file, [
+                    $customer->customer_id,
+                    $customer->customer->email ?? '',
+                    $customer->customer->first_name . ' ' . $customer->customer->last_name,
+                    $customer->mw_reward_point,
+                    $customer->last_checkout
+                ]);
+            }
+
+            fclose($file);
+        };
+
+        return response()->stream($callback, 200, $headers);
+    }
+
+
+}

+ 169 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/ReportController.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace Longyi\RewardPoints\Http\Controllers\Admin;
+
+use Illuminate\Http\Request;
+use Webkul\Admin\Http\Controllers\Controller;
+use Longyi\RewardPoints\Models\RewardPointHistory;
+use Longyi\RewardPoints\Models\RewardPointCustomer;
+use Longyi\RewardPoints\Models\RewardPointCustomerSign;
+use Carbon\Carbon;
+
+class ReportController extends Controller
+{
+    protected $_config;
+
+    public function __construct()
+    {
+        $this->_config = request('_config') ?: [];
+    }
+
+    /**
+     * Display reward points reports
+     */
+    public function index()
+    {
+        $startDate = request()->get('start_date', Carbon::now()->subDays(30)->format('Y-m-d'));
+        $endDate = request()->get('end_date', Carbon::now()->format('Y-m-d'));
+
+        $data = $this->getReportData($startDate, $endDate);
+
+        $view = $this->_config['view'] ?? 'rewardpoints::admin.reports.index';
+
+        return view($view, compact('data', 'startDate', 'endDate'));
+    }
+
+    /**
+     * Get report data
+     */
+    protected function getReportData($startDate, $endDate)
+    {
+        $startDateTime = Carbon::parse($startDate)->startOfDay();
+        $endDateTime = Carbon::parse($endDate)->endOfDay();
+
+        // Points earned by type
+        $pointsByType = RewardPointHistory::whereBetween('transaction_time', [$startDateTime, $endDateTime])
+            ->where('amount', '>', 0)
+            ->selectRaw('type_of_transaction, SUM(amount) as total_points, COUNT(*) as total_transactions')
+            ->groupBy('type_of_transaction')
+            ->get();
+
+        // Daily sign-in statistics
+        $signIns = RewardPointCustomerSign::whereBetween('created', [$startDateTime, $endDateTime])
+            ->selectRaw('DATE(created) as date, COUNT(*) as total_sign_ins, SUM(point) as total_points_earned')
+            ->groupBy('date')
+            ->orderBy('date', 'desc')
+            ->limit(30)
+            ->get();
+
+        // Points redeemed
+        $pointsRedeemed = RewardPointHistory::whereBetween('transaction_time', [$startDateTime, $endDateTime])
+            ->where('amount', '<', 0)
+            ->sum('amount');
+
+        // Top customers by points
+        $topCustomers = RewardPointCustomer::with('customer')
+            ->orderBy('mw_reward_point', 'desc')
+            ->limit(10)
+            ->get();
+
+        // Overall statistics
+        $totalPointsEarned = RewardPointHistory::whereBetween('transaction_time', [$startDateTime, $endDateTime])
+            ->where('amount', '>', 0)
+            ->sum('amount');
+
+        $totalTransactions = RewardPointHistory::whereBetween('transaction_time', [$startDateTime, $endDateTime])
+            ->count();
+
+        $totalCustomersWithPoints = RewardPointCustomer::count();
+
+        return [
+            'points_by_type' => $pointsByType,
+            'sign_ins' => $signIns,
+            'points_redeemed' => abs($pointsRedeemed),
+            'top_customers' => $topCustomers,
+            'total_points_earned' => $totalPointsEarned,
+            'total_transactions' => $totalTransactions,
+            'total_customers' => $totalCustomersWithPoints
+        ];
+    }
+
+    /**
+     * Export report
+     */
+    public function export(Request $request)
+    {
+        $startDate = $request->get('start_date', Carbon::now()->subDays(30)->format('Y-m-d'));
+        $endDate = $request->get('end_date', Carbon::now()->format('Y-m-d'));
+
+        $data = $this->getReportData($startDate, $endDate);
+
+        $filename = "reward_points_report_{$startDate}_{$endDate}.csv";
+
+        $headers = [
+            'Content-Type' => 'text/csv',
+            'Content-Disposition' => "attachment; filename={$filename}",
+        ];
+
+        $callback = function() use ($data, $startDate, $endDate) {
+            $file = fopen('php://output', 'w');
+
+            // Summary
+            fputcsv($file, ['Reward Points Report']);
+            fputcsv($file, ['Date Range', $startDate, $endDate]);
+            fputcsv($file, []);
+
+            fputcsv($file, ['Summary Statistics']);
+            fputcsv($file, ['Total Points Earned', $data['total_points_earned']]);
+            fputcsv($file, ['Points Redeemed', $data['points_redeemed']]);
+            fputcsv($file, ['Total Transactions', $data['total_transactions']]);
+            fputcsv($file, ['Total Customers with Points', $data['total_customers']]);
+            fputcsv($file, []);
+
+            // Points by type
+            fputcsv($file, ['Points by Transaction Type']);
+            fputcsv($file, ['Type', 'Total Points', 'Number of Transactions']);
+
+            $typeNames = [
+                1 => 'Order',
+                2 => 'Registration',
+                3 => 'Product Review',
+                4 => 'Daily Sign In',
+                5 => 'Referral',
+                6 => 'Birthday',
+                0 => 'Admin Adjustment'
+            ];
+
+            foreach ($data['points_by_type'] as $item) {
+                $typeName = $typeNames[$item->type_of_transaction] ?? 'Unknown';
+                fputcsv($file, [$typeName, $item->total_points, $item->total_transactions]);
+            }
+
+            fputcsv($file, []);
+
+            // Top customers
+            fputcsv($file, ['Top 10 Customers by Points']);
+            fputcsv($file, ['Customer ID', 'Customer Name', 'Email', 'Points Balance']);
+
+            foreach ($data['top_customers'] as $customer) {
+                $name = $customer->customer ? $customer->customer->first_name . ' ' . $customer->customer->last_name : 'N/A';
+                $email = $customer->customer ? $customer->customer->email : 'N/A';
+                fputcsv($file, [$customer->customer_id, $name, $email, $customer->mw_reward_point]);
+            }
+
+            fputcsv($file, []);
+
+            // Daily sign-ins
+            fputcsv($file, ['Daily Sign-In Statistics']);
+            fputcsv($file, ['Date', 'Total Sign-Ins', 'Points Earned']);
+
+            foreach ($data['sign_ins'] as $sign) {
+                fputcsv($file, [$sign->date, $sign->total_sign_ins, $sign->total_points_earned]);
+            }
+
+            fclose($file);
+        };
+
+        return response()->stream($callback, 200, $headers);
+    }
+}

+ 490 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/RuleController.php

@@ -0,0 +1,490 @@
+<?php
+
+namespace Longyi\RewardPoints\Http\Controllers\Admin;
+
+use Exception;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
+use Illuminate\Validation\Rule;
+use Illuminate\View\View;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Webkul\Admin\Http\Controllers\Controller;
+use Webkul\Core\Models\Channel;
+use Webkul\Customer\Models\CustomerGroup;
+
+class RuleController extends Controller
+{
+    /**
+     * Transaction type mapping with complete configuration
+     */
+    private const TRANSACTION_TYPES = [
+        1 => ['name' => 'Order', 'icon' => 'icon-shopping-cart', 'color' => 'blue'],
+        2 => ['name' => 'Registration', 'icon' => 'icon-user', 'color' => 'green'],
+        3 => ['name' => 'Product Review', 'icon' => 'icon-star', 'color' => 'yellow'],
+        4 => ['name' => 'Daily Sign In', 'icon' => 'icon-calendar', 'color' => 'purple'],
+        5 => ['name' => 'Referral', 'icon' => 'icon-share', 'color' => 'indigo'],
+        6 => ['name' => 'Birthday', 'icon' => 'icon-gift', 'color' => 'pink'],
+        7 => ['name' => 'Share', 'icon' => 'icon-share-alt', 'color' => 'orange'],
+        8 => ['name' => 'Subscribe', 'icon' => 'icon-envelope', 'color' => 'orange'],
+    ];
+
+    /**
+     * Valid transaction type IDs
+     */
+    private const VALID_TRANSACTION_TYPES = [1, 2, 3, 4, 5, 6, 7, 8];
+
+    /**
+     * Color classes mapping for transaction types
+     */
+    private const COLOR_CLASSES = [
+        'blue' => 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300',
+        'green' => 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300',
+        'yellow' => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300',
+        'purple' => 'bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300',
+        'indigo' => 'bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300',
+        'pink' => 'bg-pink-100 dark:bg-pink-900 text-pink-700 dark:text-pink-300',
+        'orange' => 'bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300',
+        'gray' => 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400',
+    ];
+
+    protected RewardPointRepository $rewardPointRepository;
+    
+    protected array $_config;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        $this->rewardPointRepository = $rewardPointRepository;
+        $this->_config = request('_config', []);
+    }
+
+    /**
+     * Display a listing of reward rules
+     */
+    public function index(): View
+    {
+        $rules = RewardActiveRule::query()
+            ->orderBy('rule_id', 'desc')
+            ->paginate(15);
+        
+        $customerGroupsList = $this->getCustomerGroups();
+        $transactionTypes = self::TRANSACTION_TYPES;
+        $colorClasses = self::COLOR_CLASSES;
+
+        $view = $this->_config['view'] ?? 'rewardpoints::admin.rules.index';
+        
+        return view($view, compact('rules', 'customerGroupsList', 'transactionTypes', 'colorClasses'));
+    }
+
+    /**
+     * Show the form for creating a new reward rule
+     */
+    public function create(): View
+    {
+        $view = $this->_config['view'] ?? 'rewardpoints::admin.rules.create';
+        
+        return view($view, [
+            'transactionTypes' => self::TRANSACTION_TYPES,
+            'storeViews' => $this->getStoreViews(),
+            'customerGroups' => $this->getCustomerGroups(),
+            'colorClasses' => self::COLOR_CLASSES,
+        ]);
+    }
+
+
+    /**
+     * Show the form for editing the specified reward rule
+     */
+    public function edit(int $id): View
+    {
+        $rule = RewardActiveRule::findOrFail($id);
+
+        // 处理 Store Views 数据
+        $selectedStoreViews = $this->parseStoreViews($rule);
+        
+        // 如果规则适用于所有店铺(store_view = '0'),则转换为 'all' 选项
+        $selectedStoreViewsForSelect = [];
+        if ($rule->store_view === '0' || empty($selectedStoreViews)) {
+            $selectedStoreViewsForSelect = ['all'];
+        } else {
+            $selectedStoreViewsForSelect = $selectedStoreViews;
+        }
+        
+        // 处理客户群组数据
+        $selectedCustomerGroupsArray = $this->parseCustomerGroups($rule);
+        $selectedCustomerGroupsPoints = $rule->enable_different_points_by_group 
+            ? (json_decode($rule->customer_group_ids, true) ?: [])
+            : [];
+
+        $view = $this->_config['view'] ?? 'rewardpoints::admin.rules.edit';
+        
+        return view($view, [
+            'rule' => $rule,
+            'transactionTypes' => self::TRANSACTION_TYPES,
+            'storeViews' => $this->getStoreViews(),
+            'customerGroups' => $this->getCustomerGroups(),
+            'selectedStoreViewsForSelect' => $selectedStoreViewsForSelect,
+            'selectedCustomerGroupsArray' => $selectedCustomerGroupsArray,
+            'selectedCustomerGroupsPoints' => $selectedCustomerGroupsPoints,
+            'colorClasses' => self::COLOR_CLASSES,
+        ]);
+    }
+
+    /**
+     * Store a newly created reward rule
+     */
+    public function store(Request $request): RedirectResponse
+    {
+        $validated = $this->validateStoreRequest($request);
+
+        try {
+            $data = $this->prepareDataForSave($request, $validated);
+            RewardActiveRule::create($data);
+
+            session()->flash('success', 'Reward rule created successfully.');
+            
+            return $this->redirectToIndex();
+        } catch (Exception $e) {
+            session()->flash('error', 'Error creating reward rule: ' . $e->getMessage());
+            return redirect()->back()->withInput();
+        }
+    }
+
+   
+    /**
+     * Update the specified reward rule
+     */
+    public function update(Request $request, int $id): RedirectResponse
+    {
+        $validated = $this->validateUpdateRequest($request);
+        $rule = RewardActiveRule::findOrFail($id);
+
+        try {
+            $data = $this->prepareDataForSave($request, $validated);
+            
+            // 确保 enable_different_points_by_group 字段被正确设置
+            $data['enable_different_points_by_group'] = $request->boolean('enable_different_points_by_group');
+            
+            $rule->update($data);
+
+            session()->flash('success', 'Reward rule updated successfully.');
+            
+            return $this->redirectToIndex();
+        } catch (Exception $e) {
+            session()->flash('error', 'Error updating reward rule: ' . $e->getMessage());
+            return redirect()->back()->withInput();
+        }
+    }
+
+    /**
+     * Remove the specified reward rule
+     */
+    public function destroy(int $id): JsonResponse|RedirectResponse
+    {
+        try {
+            $rule = RewardActiveRule::findOrFail($id);
+            $rule->delete();
+
+            $message = 'Reward rule deleted successfully.';
+            session()->flash('success', $message);
+
+            if (request()->ajax()) {
+                return response()->json(['status' => true, 'message' => $message]);
+            }
+
+            return $this->redirectToIndex();
+        } catch (Exception $e) {
+            $message = 'Error deleting reward rule: ' . $e->getMessage();
+            session()->flash('error', $message);
+
+            if (request()->ajax()) {
+                return response()->json(['status' => false, 'message' => 'Error deleting reward rule.'], 500);
+            }
+
+            return $this->redirectToIndex()->with('error', $message);
+        }
+    }
+
+    /**
+     * Update rule status
+     */
+    public function updateStatus(int $id, Request $request): JsonResponse
+    {
+        try {
+            $rule = RewardActiveRule::findOrFail($id);
+            $rule->status = $request->input('status', 0);
+            $rule->save();
+
+            return response()->json([
+                'status' => true,
+                'message' => 'Rule status updated successfully.',
+            ]);
+        } catch (Exception $e) {
+            return response()->json([
+                'status' => false,
+                'message' => 'Error updating rule status.',
+            ], 500);
+        }
+    }
+
+    /**
+     * Validate store request
+     */
+    private function validateStoreRequest(Request $request): array
+    {
+        $rules = [
+            'rule_name' => 'required|string|max:255',
+            'type_of_transaction' => ['required', 'integer', Rule::in(self::VALID_TRANSACTION_TYPES)],
+            'status' => 'required|boolean',
+            'enable_different_points_by_group' => 'nullable|boolean',
+            'expired_day' => 'nullable|integer|min:0',
+            'comment' => 'nullable|string|max:500',
+            'store_view' => 'nullable|array',
+            'store_view.*' => 'nullable|string',
+        ];
+
+        return $request->validate($rules);
+    }
+
+    /**
+     * Validate update request
+     */
+    private function validateUpdateRequest(Request $request): array
+    {
+        $rules = [
+            'rule_name' => 'required|string|max:255',
+            'type_of_transaction' => ['required', 'integer', Rule::in(self::VALID_TRANSACTION_TYPES)],
+            'status' => 'required|boolean',
+            'enable_different_points_by_group' => 'nullable|boolean',
+            'default_expired' => 'nullable|boolean',
+            'expired_day' => 'nullable|integer|min:0',
+            'coupon_code' => 'nullable|string|max:255',
+            'comment' => 'nullable|string|max:500',
+            'store_view' => 'nullable|array',
+            'store_view.*' => 'nullable|string',
+        ];
+
+        return $request->validate($rules);
+    }
+
+    /**
+     * Prepare data for saving
+     */
+    private function prepareDataForSave(Request $request, array $validated): array
+    {
+        $data = array_intersect_key($validated, array_flip([
+            'rule_name',
+            'type_of_transaction',
+            'default_expired',
+            'expired_day',
+            'date_event',
+            'comment',
+            'coupon_code',
+            'status',
+            'enable_different_points_by_group',
+        ]));
+
+        // Handle store views - 修复:确保正确处理 store_view
+        $storeViews = $request->get('store_view', []);
+        $data['store_view'] = $this->formatStoreViews($storeViews);
+
+        // Handle points configuration
+        $this->handlePointsConfiguration($request, $data);
+
+        return $data;
+    }
+
+    /**
+     * Format store views for database storage
+     */
+    private function formatStoreViews($storeViews): string
+{
+    // 如果 storeViews 为空或为 '0',返回 '0' 表示所有店铺
+    if (empty($storeViews) || $storeViews === '0') {
+        return '0';
+    }
+    
+    // 如果是单个值,直接返回
+    if (is_scalar($storeViews) && !empty($storeViews)) {
+        return (string)$storeViews;
+    }
+    
+    return '0';
+}
+
+    /**
+     * Handle points configuration based on group settings
+     */
+    private function handlePointsConfiguration(Request $request, array &$data): void
+    {
+        if ($request->boolean('enable_different_points_by_group')) {
+            $groupPoints = $request->get('group_points', []);
+            
+            // 确保 $groupPoints 是数组
+            if (!is_array($groupPoints)) {
+                $groupPoints = [];
+            }
+            
+            // 过滤并处理群组积分数据,确保键是字符串或整数
+            $filteredPoints = [];
+            foreach ($groupPoints as $groupId => $points) {
+                // 确保键是标量类型且不为空
+                if (is_scalar($groupId) && !empty($groupId)) {
+                    // 确保积分值是数字
+                    $pointsValue = is_numeric($points) ? (int)$points : 0;
+                    if ($pointsValue >= 0) {
+                        $filteredPoints[(string)$groupId] = $pointsValue;
+                    }
+                }
+            }
+            
+            $data['customer_group_ids'] = !empty($filteredPoints) ? json_encode($filteredPoints) : '{}';
+            $data['reward_point'] = 0;
+        } else {
+            $request->validate([
+                'reward_point' => 'required|integer|min:0',
+            ]);
+            $data['reward_point'] = (int)$request->get('reward_point');
+            $data['customer_group_ids'] = $this->formatCustomerGroups($request->get('customer_group_ids', []));
+        }
+    }
+
+    /**
+     * Format customer groups for database storage
+     */
+    private function formatCustomerGroups($customerGroups): string
+    {
+        if (is_array($customerGroups) && !empty($customerGroups)) {
+            // 过滤掉空值并确保所有值都是标量
+            $filtered = [];
+            foreach ($customerGroups as $groupId) {
+                if (is_scalar($groupId) && !empty($groupId)) {
+                    $filtered[] = (string)$groupId;
+                }
+            }
+            return !empty($filtered) ? implode(',', array_unique($filtered)) : '';
+        }
+        
+        return '';
+    }
+
+    /**
+     * Parse store views from rule
+     */
+    private function parseStoreViews(RewardActiveRule $rule): array
+    {
+        if ($rule->store_view && $rule->store_view !== '0') {
+            $views = explode(',', $rule->store_view);
+            // 过滤空值
+            return array_filter($views, function($view) {
+                return !empty($view);
+            });
+        }
+        
+        return [];
+    }
+
+    /**
+     * Parse customer groups from rule
+     */
+    private function parseCustomerGroups(RewardActiveRule $rule): array
+    {
+        if ($rule->enable_different_points_by_group) {
+            $decoded = json_decode($rule->customer_group_ids, true);
+            if (is_array($decoded)) {
+                // 确保解码后的数据格式正确
+                $result = [];
+                foreach ($decoded as $groupId => $points) {
+                    if (is_scalar($groupId) && !empty($groupId)) {
+                        $result[(string)$groupId] = is_numeric($points) ? (int)$points : 0;
+                    }
+                }
+                return $result;
+            }
+            return [];
+        }
+        
+        if ($rule->customer_group_ids && $rule->customer_group_ids !== '') {
+            $groups = explode(',', $rule->customer_group_ids);
+            // 过滤空值
+            return array_filter($groups, function($group) {
+                return !empty($group);
+            });
+        }
+        
+        return [];
+    }
+
+    /**
+     * Get all store views
+     */
+    protected function getStoreViews(): array
+    {
+        try {
+            $channels = Channel::all();
+            $storeViews = [];
+            
+            foreach ($channels as $channel) {
+                $storeViews[$channel->code] = $channel->name;
+            }
+            
+            return $storeViews;
+        } catch (Exception $e) {
+            return ['0' => 'All Stores'];
+        }
+    }
+
+    /**
+     * Get all customer groups
+     */
+    protected function getCustomerGroups(): array
+    {
+        try {
+            $groups = CustomerGroup::orderBy('id')->get();
+            $customerGroups = [];
+            
+            foreach ($groups as $group) {
+                $customerGroups[$group->id] = $group->name;
+            }
+            
+            return $customerGroups;
+        } catch (Exception $e) {
+            return ['0' => 'All Groups'];
+        }
+    }
+
+    /**
+     * Get transaction type configuration
+     */
+    public function getTransactionTypeConfig(): array
+    {
+        return self::TRANSACTION_TYPES;
+    }
+
+    /**
+     * Get transaction type name
+     */
+    protected function getTransactionTypeName(int $typeId): string
+    {
+        return self::TRANSACTION_TYPES[$typeId]['name'] ?? 'Unknown';
+    }
+
+    /**
+     * Get transaction type color classes
+     */
+    protected function getTransactionTypeColorClass(int $typeId): string
+    {
+        $color = self::TRANSACTION_TYPES[$typeId]['color'] ?? 'gray';
+        return self::COLOR_CLASSES[$color] ?? self::COLOR_CLASSES['gray'];
+    }
+
+    /**
+     * Redirect to index route
+     */
+    private function redirectToIndex(): RedirectResponse
+    {
+        $route = $this->_config['redirect'] ?? 'admin.reward-points.rules.index';
+        return redirect()->route($route);
+    }
+}

+ 105 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/SettingController.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace Longyi\RewardPoints\Http\Controllers\Admin;
+
+use Illuminate\Http\Request;
+use Webkul\Admin\Http\Controllers\Controller;
+use Longyi\RewardPoints\Repositories\RewardPointSettingRepository;
+
+class SettingController extends Controller
+{
+    protected $settingRepository;
+    protected $_config;
+
+    public function __construct(
+        RewardPointSettingRepository $settingRepository
+    ) {
+        $this->settingRepository = $settingRepository;
+        $this->_config = request('_config') ?: [];
+    }
+
+    /**
+     * 显示配置页面
+     */
+    public function index()
+    {
+        $groups = $this->settingRepository->getGroups();
+        $currentGroup = request('group', 'general');
+        $settings = $this->settingRepository->getSettingsByGroup($currentGroup);
+        
+        // 获取分组数据(包含名称和排序)
+        $groupData = $this->getGroupData($groups);
+        
+        // 确保 _config 存在且包含必要的键
+        $view = isset($this->_config['view']) ? $this->_config['view'] : 'rewardpoints::admin.settings.index';
+        
+        return view($view, compact('groups', 'currentGroup', 'settings', 'groupData'));
+    }
+
+    /**
+     * 保存配置
+     */
+    public function save(Request $request)
+    {
+        $this->validate($request, [
+            'settings' => 'required|array'
+        ]);
+        
+        try {
+            $settings = $request->input('settings');
+            
+            foreach ($settings as $code => $value) {
+                $this->settingRepository->setConfigValue($code, $value);
+            }
+            
+            // 清除缓存
+            cache()->forget('reward_points_settings');
+            
+            session()->flash('success', '配置保存成功');
+            
+            $redirectRoute = isset($this->_config['redirect']) ? $this->_config['redirect'] : 'admin.reward-points.settings.index';
+            return redirect()->route($redirectRoute, ['group' => $request->input('group', 'general')]);
+            
+        } catch (\Exception $e) {
+            session()->flash('error', '保存配置失败:' . $e->getMessage());
+            return redirect()->back()->withInput();
+        }
+    }
+    
+    /**
+     * 获取分组数据(包含名称和排序)
+     */
+    protected function getGroupData($groups)
+    {
+        // 定义分组排序顺序
+        $groupOrder = [
+            'general' => 1,      // 通用设置
+        ];
+        
+        $groupData = [];
+        foreach ($groups as $group) {
+            $groupData[$group] = [
+                'name' => $this->getGroupName($group),
+                'order' => $groupOrder[$group] ?? 99, // 如果未定义顺序,默认排在最后
+            ];
+        }
+        
+        // 按照排序顺序排列
+        uasort($groupData, function($a, $b) {
+            return $a['order'] <=> $b['order'];
+        });
+        
+        return $groupData;
+    }
+    
+    /**
+     * 获取分组名称
+     */
+    protected function getGroupName($group)
+    {
+        $names = [
+            'general' => '通用设置',
+        ];
+        return $names[$group] ?? ucfirst($group);
+    }
+}

+ 202 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/TransactionController.php

@@ -0,0 +1,202 @@
+<?php
+
+namespace Longyi\RewardPoints\Http\Controllers\Admin;
+
+use Illuminate\Http\Request;
+use Webkul\Admin\Http\Controllers\Controller;
+use Longyi\RewardPoints\Models\RewardPointHistory;
+use Longyi\RewardPoints\Models\RewardPointCustomer;
+use Carbon\Carbon;
+use Maatwebsite\Excel\Facades\Excel;
+use Longyi\RewardPoints\Exports\TransactionsExport;
+
+class TransactionController extends Controller
+{
+    protected $_config;
+
+    public function __construct()
+    {
+        $this->_config = request('_config') ?: [];
+    }
+
+    public function earnedIndex()
+    {
+        request()->merge(['view_type' => 'earned']);
+        return $this->index();
+    }
+
+    /**
+     * Display a listing of reward point transactions
+     */
+    public function index()
+    {
+        $startDate = request()->get('start_date', Carbon::now()->subDays(30)->format('Y-m-d'));
+        $endDate = request()->get('end_date', Carbon::now()->format('Y-m-d'));
+        $customerEmail = request()->get('customer_email');
+        $transactionType = request()->get('transaction_type');
+        $amountType = request()->get('amount_type'); // all, earned, redeemed
+        $viewType = request()->get('view_type', 'all'); // all or earned
+
+        $query = RewardPointHistory::with('customer');
+
+        // 日期筛选
+        if ($startDate && $endDate) {
+            $startDateTime = Carbon::parse($startDate)->startOfDay();
+            $endDateTime = Carbon::parse($endDate)->endOfDay();
+            $query->whereBetween('transaction_time', [$startDateTime, $endDateTime]);
+        }
+
+        // 客户邮箱筛选
+        if ($customerEmail) {
+            $query->whereHas('customer', function ($q) use ($customerEmail) {
+                $q->where('email', 'like', "%{$customerEmail}%");
+            });
+        }
+
+        // 交易类型筛选
+        if ($transactionType !== null && $transactionType !== '') {
+            $query->where('type_of_transaction', $transactionType);
+        }
+
+        // 根据视图类型筛选(仅获取或全部)
+        if ($viewType === 'earned') {
+            $query->where('amount', '>', 0);
+        } elseif ($amountType === 'earned') {
+            $query->where('amount', '>', 0);
+        } elseif ($amountType === 'redeemed') {
+            $query->where('amount', '<', 0);
+        }
+
+        $transactions = $query->orderBy('transaction_time', 'desc')->paginate(20);
+
+        // 统计数据 - 根据视图类型调整
+        if ($viewType === 'earned') {
+            $totalEarned = (clone $query)->sum('amount');
+            $totalRedeemed = 0;
+            $totalTransactions = (clone $query)->count();
+        } else {
+            $totalEarned = (clone $query)->where('amount', '>', 0)->sum('amount');
+            $totalRedeemed = abs((clone $query)->where('amount', '<', 0)->sum('amount'));
+            $totalTransactions = (clone $query)->count();
+        }
+
+        $view = isset($this->_config['view']) ? $this->_config['view'] : 'rewardpoints::admin.transactions.index';
+
+        return view($view, compact(
+            'transactions',
+            'startDate',
+            'endDate',
+            'customerEmail',
+            'transactionType',
+            'amountType',
+            'viewType',
+            'totalEarned',
+            'totalRedeemed',
+            'totalTransactions'
+        ));
+    }
+// ... existing code ...
+
+
+    /**
+     * Export transactions
+     */
+    public function export(Request $request)
+    {
+        $startDate = $request->get('start_date', Carbon::now()->subDays(30)->format('Y-m-d'));
+        $endDate = $request->get('end_date', Carbon::now()->format('Y-m-d'));
+        $customerEmail = $request->get('customer_email');
+        $transactionType = $request->get('transaction_type');
+        $amountType = $request->get('amount_type');
+
+        $query = RewardPointHistory::with('customer');
+
+        // 应用相同的筛选条件
+        if ($startDate && $endDate) {
+            $startDateTime = Carbon::parse($startDate)->startOfDay();
+            $endDateTime = Carbon::parse($endDate)->endOfDay();
+            $query->whereBetween('transaction_time', [$startDateTime, $endDateTime]);
+        }
+
+        if ($customerEmail) {
+            $query->whereHas('customer', function ($q) use ($customerEmail) {
+                $q->where('email', 'like', "%{$customerEmail}%");
+            });
+        }
+
+        if ($transactionType !== null && $transactionType !== '') {
+            $query->where('type_of_transaction', $transactionType);
+        }
+
+        if ($amountType === 'earned') {
+            $query->where('amount', '>', 0);
+        } elseif ($amountType === 'redeemed') {
+            $query->where('amount', '<', 0);
+        }
+
+        $transactions = $query->orderBy('transaction_time', 'desc')->get();
+
+        $filename = "reward_point_transactions_" . date('Y-m-d_His') . ".csv";
+
+        $headers = [
+            'Content-Type' => 'text/csv',
+            'Content-Disposition' => "attachment; filename={$filename}",
+        ];
+
+        $callback = function() use ($transactions) {
+            $file = fopen('php://output', 'w');
+
+            // CSV 头部
+            fputcsv($file, [
+                'ID',
+                'Customer ID',
+                'Customer Name',
+                'Customer Email',
+                'Transaction Type',
+                'Amount',
+                'Balance',
+                'Transaction Time',
+                'Description',
+                'Status'
+            ]);
+
+            $typeNames = [
+                1 => 'Order',
+                2 => 'Registration',
+                3 => 'Product Review',
+                4 => 'Daily Sign In',
+                5 => 'Referral',
+                6 => 'Birthday',
+                8 => 'Subscribe',
+                0 => 'Admin Adjustment',
+                99 => 'Admin Action'
+            ];
+
+            $statusNames = [
+                0 => 'Pending',
+                1 => 'Completed',
+                2 => 'Cancelled',
+                3 => 'Expired'
+            ];
+
+            foreach ($transactions as $transaction) {
+                fputcsv($file, [
+                    $transaction->history_id,
+                    $transaction->customer_id,
+                    $transaction->customer ? $transaction->customer->first_name . ' ' . $transaction->customer->last_name : 'N/A',
+                    $transaction->customer ? $transaction->customer->email : 'N/A',
+                    $typeNames[$transaction->type_of_transaction] ?? 'Unknown',
+                    $transaction->amount,
+                    $transaction->balance,
+                    $transaction->transaction_time,
+                    $transaction->transaction_detail,
+                    $statusNames[$transaction->status] ?? 'Unknown'
+                ]);
+            }
+
+            fclose($file);
+        };
+
+        return response()->stream($callback, 200, $headers);
+    }
+}

+ 200 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/RewardPointsController.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace Longyi\RewardPoints\Http\Controllers;
+
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Models\RewardPointCustomerSign;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller;
+use Webkul\User\Repositories\RoleRepository;
+use Illuminate\Support\Facades\DB;
+class RewardPointsController extends Controller
+{
+    protected $rewardPointRepository;
+    protected $_config;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        $this->rewardPointRepository = $rewardPointRepository;
+        $this->_config = request('_config');
+    }
+
+    public function index()
+    {
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer) {
+            return redirect()->route('customer.session.index');
+        }
+
+        $points = $this->rewardPointRepository->getCustomerPoints($customer->id);
+        $history = $this->rewardPointRepository->getHistory($customer->id);
+
+        return view($this->_config['view'], compact('points', 'history'));
+    }
+
+    public function signIn()
+    {
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer) {
+            return response()->json(['error' => 'Please login first'], 401);
+        }
+
+        $today = Carbon::now()->format('Y-m-d');
+
+        $existingSign = RewardPointCustomerSign::where('customer_id', $customer->id)
+            ->where('sign_date', $today)
+            ->first();
+
+        if ($existingSign) {
+            return response()->json(['error' => 'Already signed in today'], 400);
+        }
+
+        $lastSign = RewardPointCustomerSign::where('customer_id', $customer->id)
+            ->orderBy('sign_date', 'desc')
+            ->first();
+
+        $countDate = 1;
+        if ($lastSign) {
+            $lastSignDate = Carbon::parse($lastSign->sign_date);
+            $yesterday = Carbon::yesterday();
+            
+            if ($lastSignDate->toDateString() === $yesterday->toDateString()) {
+                $countDate = $lastSign->count_date + 1;
+            } elseif ($lastSignDate->toDateString() !== $today) {
+                $countDate = 1;
+            }
+        }
+
+        $rule = RewardActiveRule::where('type_of_transaction', RewardActiveRule::TYPE_SIGN_IN)
+            ->where('status', 1)
+            ->first();
+
+        $basePoints = config('rewardpoints.sign_in.base_points', 10);
+        $points = $rule ? $rule->reward_point : $basePoints;
+
+        if ($countDate % 30 == 0) {
+            $bonusPoints = config('rewardpoints.sign_in.month_bonus_points', 100);
+            $points += $bonusPoints;
+        } elseif ($countDate % 7 == 0) {
+            $bonusPoints = config('rewardpoints.sign_in.week_bonus_points', 20);
+            $points += $bonusPoints;
+        }
+
+        $sign = RewardPointCustomerSign::create([
+            'customer_id' => $customer->id,
+            'sign_date' => $today,
+            'count_date' => $countDate,
+            'point' => $points,
+            'code' => uniqid('SIGN_'),
+            'created' => Carbon::now(),
+            'updated' => Carbon::now()
+        ]);
+
+        $this->rewardPointRepository->addPoints(
+            $customer->id,
+            RewardActiveRule::TYPE_SIGN_IN,
+            $points,
+            null,
+            "Daily sign-in bonus (Day {$countDate})"
+        );
+
+        return response()->json([
+            'success' => true,
+            'points' => $points,
+            'streak' => $countDate,
+            'total_points' => $this->rewardPointRepository->getCustomerPoints($customer->id)
+        ]);
+    }
+    public function getSignStatus()
+    {
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer) {
+            return response()->json(['error' => 'Please login first'], 401);
+        }
+
+        $today = Carbon::now()->format('Y-m-d');
+        $signedToday = RewardPointCustomerSign::where('customer_id', $customer->id)
+            ->where('sign_date', $today)
+            ->exists();
+
+        $lastSign = RewardPointCustomerSign::where('customer_id', $customer->id)
+            ->orderBy('sign_date', 'desc')
+            ->first();
+
+        return response()->json([
+            'signed_today' => $signedToday,
+            'current_streak' => $lastSign ? $lastSign->count_date : 0,
+            'total_points' => $this->rewardPointRepository->getCustomerPoints($customer->id)
+        ]);
+    }
+
+    /**
+     * Apply reward points to cart
+     */
+    public function applyPoints(Request $request)
+    {
+        $points = $request->input('points', 0);
+        
+        $cartRewardPoints = app('cartrewardpoints');
+        $validation = $cartRewardPoints->validatePoints($points);
+        
+        if ($validation !== true) {
+            return response()->json([
+                'success' => false,
+                'message' => $validation
+            ], 400);
+        }
+        
+        $result = $cartRewardPoints->applyPoints($points);
+        
+        if ($result) {
+            $discountDetails = $cartRewardPoints->getDiscountDetails();
+            
+            return response()->json([
+                'success' => true,
+                'message' => 'Reward points applied successfully',
+                'discount' => $discountDetails
+            ]);
+        }
+        
+        return response()->json([
+            'success' => false,
+            'message' => 'Failed to apply reward points'
+        ], 400);
+    }
+
+    /**
+     * Remove reward points from cart
+     */
+    public function removePoints()
+    {
+        $cartRewardPoints = app('cartrewardpoints');
+        $cartRewardPoints->removePoints();
+        
+        return response()->json([
+            'success' => true,
+            'message' => 'Reward points removed successfully'
+        ]);
+    }
+
+    /**
+     * Get points information for cart
+     */
+    public function getPointsInfo()
+    {
+        $cartRewardPoints = app('cartrewardpoints');
+        
+        return response()->json([
+            'available_points' => $cartRewardPoints->getAvailablePoints(),
+            'points_used' => $cartRewardPoints->getPointsUsed(),
+            'discount_amount' => $cartRewardPoints->getDiscountAmount(),
+            'points_value' => $cartRewardPoints->getPointsValue(1),
+            'max_points_allowed' => $cartRewardPoints->getMaxPointsByCartTotal()
+        ]);
+    }
+}

+ 118 - 0
packages/Longyi/RewardPoints/src/Listeners/CustomerEvents.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace Longyi\RewardPoints\Listeners;
+
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+use Webkul\Customer\Models\Customer;
+use Illuminate\Support\Facades\Log;
+
+class CustomerEvents
+{
+    protected $rewardPointRepository;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        $this->rewardPointRepository = $rewardPointRepository;
+    }
+
+    public function handleCustomerRegistration($event)
+    {
+        // 检查事件参数类型
+        if (is_object($event) && method_exists($event, 'getAttribute')) {
+            $customer = $event;
+        } elseif (is_array($event) && isset($event['customer'])) {
+            $customer = $event['customer'];
+        } elseif (is_array($event) && isset($event['id'])) {
+            $customer = Customer::find($event['id']);
+        } elseif (is_int($event) || is_string($event)) {
+            $customer = Customer::find($event);
+        } else {
+            $customer = $event;
+        }
+
+        if (!$customer) {
+            Log::warning('Customer not found in registration event');
+            return;
+        }
+
+        // 查询注册规则
+        $registrationRule = RewardActiveRule::where('type_of_transaction', RewardActiveRule::TYPE_REGISTRATION)
+            ->where('status', 1)
+            ->first();
+
+        // 查询订阅规则
+        $subscribeRule = RewardActiveRule::where('type_of_transaction', RewardActiveRule::TYPE_SUBSCRIBE)
+            ->where('status', 1)
+            ->first();
+
+        // 准备批量添加的积分列表
+        $pointsList = [];
+
+        // 添加注册积分
+        $registrationPoints = $this->getPointsFromRuleOrConfig(
+            $registrationRule,
+            'rewardpoints.registration.points_per_registration',
+            100
+        );
+        
+        if ($registrationPoints > 0) {
+            $pointsList[] = [
+                'amount' => $registrationPoints,
+                'type' => RewardActiveRule::TYPE_REGISTRATION,
+                'detail' => 'Registration bonus',
+                'rule' => $registrationRule
+            ];
+        }
+
+        // 检查客户是否订阅了新闻通讯
+        $isSubscribedToNewsletter = false;
+        if (property_exists($customer, 'subscribed_to_news_letter')) {
+            $isSubscribedToNewsletter = $customer->subscribed_to_news_letter;
+        } elseif (method_exists($customer, 'getAttribute')) {
+            $isSubscribedToNewsletter = $customer->getAttribute('subscribed_to_news_letter');
+        }
+
+        // 如果用户订阅了新闻通讯,添加订阅积分
+        if ($isSubscribedToNewsletter) {
+            $subscribePoints = $this->getPointsFromRuleOrConfig(
+                $subscribeRule,
+                'rewardpoints.newsletter_subscribe.points_per_subscription',
+                200
+            );
+            
+            if ($subscribePoints > 0) {
+                $pointsList[] = [
+                    'amount' => $subscribePoints,
+                    'type' => RewardActiveRule::TYPE_SUBSCRIBE,
+                    'detail' => 'Newsletter subscription bonus',
+                    'rule' => $subscribeRule
+                ];
+            }
+        }
+
+        // 如果有积分需要添加,使用批量添加方法
+        if (!empty($pointsList)) {
+            $histories = $this->rewardPointRepository->addPointsBatch($customer->id, $pointsList);
+            
+            Log::info('Points added for customer registration', [
+                'customer_id' => $customer->id,
+                'points_count' => count($pointsList),
+                'points_breakdown' => array_column($pointsList, 'amount'),
+                'total_points' => array_sum(array_column($pointsList, 'amount'))
+            ]);
+        }
+    }
+
+    /**
+     * 从规则或配置获取积分值
+     */
+    private function getPointsFromRuleOrConfig($rule, $configKey, $defaultValue)
+    {
+        if ($rule && $rule->reward_point > 0) {
+            return (int) $rule->reward_point;
+        }
+        
+        return (int) config($configKey, $defaultValue);
+    }
+}

+ 87 - 0
packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace Longyi\RewardPoints\Listeners;
+
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+
+class OrderEvents
+{
+    protected $rewardPointRepository;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        $this->rewardPointRepository = $rewardPointRepository;
+    }
+
+    public function handleOrderPlacement($order)
+    {
+        if (!$order->customer_id) {
+            return;
+        }
+
+        $rule = RewardActiveRule::where('type_of_transaction', RewardActiveRule::TYPE_ORDER)
+            ->where('status', 1)
+            ->first();
+
+        if (!$rule) {
+            return;
+        }
+
+        $points = $this->calculateOrderPoints($order, $rule);
+
+        if ($points > 0) {
+            $this->rewardPointRepository->addPoints(
+                $order->customer_id,
+                RewardActiveRule::TYPE_ORDER,
+                $points,
+                $order->id,
+                "Points earned from order #{$order->increment_id}"
+            );
+        }
+    }
+
+     public function handleOrderCancellation($order)
+    {
+        if (!$order->customer_id) {
+            return;
+        }
+
+        \DB::transaction(function () use ($order) {
+            $histories = $this->rewardPointRepository->findWhere([
+                'customer_id' => $order->customer_id,
+                'history_order_id' => $order->id,
+                'type_of_transaction' => RewardActiveRule::TYPE_ORDER,
+                'status' => RewardPointHistory::STATUS_COMPLETED
+            ])->get();
+
+            if ($histories->isEmpty()) {
+                return;
+            }
+
+            foreach ($histories as $history) {
+                if ($history->amount > 0 && $history->point_remaining > 0) {
+                    $result = $this->rewardPointRepository->deductPoints(
+                        $order->customer_id,
+                        $history->point_remaining,
+                        $order->id,
+                        "Points deducted due to order cancellation #{$order->increment_id}"
+                    );
+
+                    if ($result) {
+                        $history->status = RewardPointHistory::STATUS_CANCELLED;
+                        $history->point_remaining = 0;
+                        $history->save();
+                    }
+                }
+            }
+        });
+    }
+    protected function calculateOrderPoints($order, $rule)
+    {
+        // Simple calculation based on order grand total
+        // You can implement more complex logic based on your requirements
+        $pointsPerCurrency = 10; // 10 points per currency unit
+        return floor($order->grand_total * $pointsPerCurrency);
+    }
+}

+ 33 - 0
packages/Longyi/RewardPoints/src/Listeners/ReviewEvents.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Longyi\RewardPoints\Listeners;
+
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+
+class ReviewEvents
+{
+    protected $rewardPointRepository;
+
+    public function __construct(RewardPointRepository $rewardPointRepository)
+    {
+        $this->rewardPointRepository = $rewardPointRepository;
+    }
+
+    public function handleReviewCreation($review)
+    {
+        $rule = RewardActiveRule::where('type_of_transaction', RewardActiveRule::TYPE_REVIEW)
+            ->where('status', 1)
+            ->first();
+
+        if ($rule && $rule->reward_point > 0 && $review->customer_id) {
+            $this->rewardPointRepository->addPoints(
+                $review->customer_id,
+                RewardActiveRule::TYPE_REVIEW,
+                $rule->reward_point,
+                null,
+                "Points earned for product review"
+            );
+        }
+    }
+}

+ 95 - 0
packages/Longyi/RewardPoints/src/Models/RewardActiveRule.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class RewardActiveRule extends Model
+{
+    // 定义交易类型常量
+    const TYPE_REGISTRATION = 2;   // 注册
+    const TYPE_SIGN_IN = 1;        // 登录
+    const TYPE_ORDER = 3;          // 订单
+    const TYPE_REVIEW = 4;         // 评价
+    const TYPE_REFERRAL = 5;       // 推荐
+    const TYPE_BIRTHDAY = 6;       // 生日
+    const TYPE_SHARE  = 7;         // 分享
+    const TYPE_SUBSCRIBE  = 8;         // 关注
+    protected $table = 'mw_reward_active_rules';
+
+    protected $primaryKey = 'rule_id';
+
+    public $timestamps = false;
+
+    protected $fillable = [
+        'rule_name',
+        'type_of_transaction',
+        'store_view',
+        'customer_group_ids',
+        'enable_different_points_by_group',
+        'default_expired',
+        'expired_day',
+        'date_event',
+        'comment',
+        'coupon_code',
+        'reward_point',
+        'status',
+    ];
+
+    protected $casts = [
+        'type_of_transaction' => 'integer',
+        'status' => 'boolean',
+        'expired_day' => 'integer',
+    ];
+
+    /**
+     * 获取客户群组积分设置
+     */
+    public function getCustomerGroupPointsAttribute()
+    {
+        if ($this->enable_different_points_by_group) {
+            $groupPoints = json_decode($this->customer_group_ids, true);
+            return is_array($groupPoints) ? $groupPoints : [];
+        }
+        return [];
+    }
+
+    /**
+     * 设置客户群组积分设置
+     */
+    public function setCustomerGroupPointsAttribute($value)
+    {
+        if ($this->enable_different_points_by_group) {
+            $this->attributes['customer_group_ids'] = json_encode($value);
+        }
+    }
+
+    /**
+     * 获取特定客户群组的积分值
+     */
+    public function getRewardPointForCustomerGroup($customerGroupId)
+    {
+        if ($this->enable_different_points_by_group) {
+            $groupPoints = $this->getCustomerGroupPointsAttribute();
+            return isset($groupPoints[$customerGroupId]) ? (int)$groupPoints[$customerGroupId] : 0;
+        }
+
+        // 如果不启用不同群组不同积分,则返回统一的积分值
+        return (int)$this->reward_point;
+    }
+     public function getTransactionTypeTextAttribute()
+    {
+        $types = [
+            self::TYPE_ORDER => 'Order',
+            self::TYPE_REGISTRATION => 'Registration',
+            self::TYPE_REVIEW => 'Product Review',
+            self::TYPE_SIGN_IN => 'Daily Sign In',
+            self::TYPE_REFERRAL => 'Referral',
+            self::TYPE_BIRTHDAY => 'Birthday',
+            self::TYPE_SHARE => 'Share',
+            self::TYPE_SUBSCRIBE => 'Subscription'
+        ];
+
+        return $types[$this->type_of_transaction] ?? 'Unknown';
+    }
+}

+ 50 - 0
packages/Longyi/RewardPoints/src/Models/RewardPointCustomer.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Webkul\Customer\Models\Customer;
+
+class RewardPointCustomer extends Model
+{
+    protected $table = 'mw_reward_point_customer';
+    protected $primaryKey = 'customer_id';
+    public $timestamps = false;
+
+    protected $fillable = [
+        'customer_id',
+        'mw_reward_point',
+        'mw_friend_id',
+        'subscribed_balance_update',
+        'subscribed_point_expiration',
+        'last_checkout'
+    ];
+
+    protected $casts = [
+        'mw_reward_point' => 'integer',
+        'mw_friend_id' => 'integer',
+        'subscribed_balance_update' => 'integer',
+        'subscribed_point_expiration' => 'integer',
+        'last_checkout' => 'datetime'
+    ];
+
+    public function customer()
+    {
+        return $this->belongsTo(Customer::class, 'customer_id');
+    }
+
+    public function friend()
+    {
+        return $this->belongsTo(Customer::class, 'mw_friend_id');
+    }
+
+    public function history()
+    {
+        return $this->hasMany(RewardPointHistory::class, 'customer_id', 'customer_id');
+    }
+
+    public function signRecords()
+    {
+        return $this->hasMany(RewardPointCustomerSign::class, 'customer_id', 'customer_id');
+    }
+}

+ 33 - 0
packages/Longyi/RewardPoints/src/Models/RewardPointCustomerSign.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class RewardPointCustomerSign extends Model
+{
+    protected $table = 'mw_reward_point_customer_sign';
+    protected $primaryKey = 'id';
+
+    protected $fillable = [
+        'customer_id',
+        'sign_date',
+        'count_date',
+        'point',
+        'code',
+        'created',
+        'updated'
+    ];
+
+    protected $casts = [
+        'count_date' => 'integer',
+        'point' => 'integer'
+    ];
+
+    protected $dates = ['created', 'updated'];
+
+    public function customer()
+    {
+        return $this->belongsTo(Customer::class, 'customer_id');
+    }
+}

+ 49 - 0
packages/Longyi/RewardPoints/src/Models/RewardPointHistory.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Webkul\Customer\Models\Customer;
+class RewardPointHistory extends Model
+{
+    protected $table = 'mw_reward_point_history';
+    protected $primaryKey = 'history_id';
+    public $timestamps = false;
+
+    protected $fillable = [
+        'customer_id',
+        'type_of_transaction',
+        'amount',
+        'balance',
+        'transaction_detail',
+        'transaction_time',
+        'history_order_id',
+        'expired_day',
+        'expired_time',
+        'point_remaining',
+        'check_time',
+        'status'
+    ];
+
+    protected $casts = [
+        'type_of_transaction' => 'integer',
+        'amount' => 'integer',
+        'balance' => 'integer',
+        'history_order_id' => 'integer',
+        'expired_day' => 'integer',
+        'point_remaining' => 'integer',
+        'check_time' => 'integer',
+        'status' => 'integer'
+    ];
+
+    protected $dates = ['transaction_time', 'expired_time'];
+
+    const STATUS_PENDING = 0;
+    const STATUS_COMPLETED = 1;
+    const STATUS_CANCELLED = 2;
+    const STATUS_EXPIRED = 3;
+    public function customer()
+    {
+        return $this->belongsTo(Customer::class, 'customer_id');
+    }
+}

+ 84 - 0
packages/Longyi/RewardPoints/src/Models/RewardPointSetting.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class RewardPointSetting extends Model
+{
+    protected $table = 'mw_reward_points_settings';
+    
+    protected $fillable = [
+        'code',
+        'name',
+        'value',
+        'type',
+        'group',
+        'sort_order',
+        'options',
+        'description',
+        'is_enabled'
+    ];
+    
+    protected $casts = [
+        'sort_order' => 'integer',
+        'is_enabled' => 'boolean',
+        'options' => 'array'
+    ];
+    
+    /**
+     * 获取配置值(自动转换类型)
+     */
+    public function getTypedValueAttribute()
+    {
+        switch ($this->type) {
+            case 'boolean':
+                return (bool) $this->value;
+            case 'number':
+                return (float) $this->value;
+            case 'select':
+                $options = $this->options ?? [];
+                return $options[$this->value] ?? $this->value;
+            default:
+                return $this->value;
+        }
+    }
+    
+    /**
+     * 按分组获取所有启用的配置
+     */
+    public static function getGroupSettings($group)
+    {
+        return self::where('group', $group)
+            ->where('is_enabled', true)
+            ->orderBy('sort_order')
+            ->get()
+            ->pluck('value', 'code')
+            ->toArray();
+    }
+    
+    /**
+     * 获取单个配置值
+     */
+    public static function getValue($code, $default = null)
+    {
+        $setting = self::where('code', $code)->first();
+        
+        if (!$setting) {
+            return $default;
+        }
+        
+        return $setting->typed_value;
+    }
+    
+    /**
+     * 设置配置值
+     */
+    public static function setValue($code, $value)
+    {
+        return self::updateOrCreate(
+            ['code' => $code],
+            ['value' => $value]
+        );
+    }
+}

+ 23 - 0
packages/Longyi/RewardPoints/src/Providers/EventServiceProvider.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Longyi\RewardPoints\Providers;
+
+use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
+
+class EventServiceProvider extends ServiceProvider
+{
+    protected $listen = [
+        'customer.registration.after' => [
+            'Longyi\RewardPoints\Listeners\CustomerEvents@handleCustomerRegistration'
+        ],
+        'checkout.order.save.after' => [
+            'Longyi\RewardPoints\Listeners\OrderEvents@handleOrderPlacement'
+        ],
+        'sales.order.cancel.after' => [
+            'Longyi\RewardPoints\Listeners\OrderEvents@handleOrderCancellation'
+        ],
+        'customer.review.create.after' => [
+            'Longyi\RewardPoints\Listeners\ReviewEvents@handleReviewCreation'
+        ],
+    ];
+}

+ 143 - 0
packages/Longyi/RewardPoints/src/Providers/RewardPointsServiceProvider.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace Longyi\RewardPoints\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Routing\Router;
+use Longyi\RewardPoints\Providers\EventServiceProvider;
+use Longyi\RewardPoints\Services\CartRewardPoints;
+use Illuminate\Support\Facades\DB;
+
+class RewardPointsServiceProvider extends ServiceProvider
+{
+    public function boot(Router $router)
+    {
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/routes.php');
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'rewardpoints');
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'rewardpoints');
+        
+        $this->publishes([
+            __DIR__ . '/../../publishable/assets' => public_path('vendor/longyi/rewardpoints'),
+        ], 'public');
+         // 修正语言文件发布路径 - 使用正确的相对路径
+        $this->publishes([
+            __DIR__ . '/../Resources/lang' => resource_path('lang/vendor/rewardpoints'),
+        ], 'lang');
+        // Publish configuration
+        $this->publishes([
+            __DIR__ . '/../Config/rewardpoints.php' => config_path('rewardpoints.php'),
+        ], 'config');
+        // 加载数据库配置覆盖文件配置
+        $this->app->booted(function () {
+            $this->loadDatabaseSettings();
+        });
+    }
+
+    public function register()
+    {
+        $this->mergeConfigFrom(
+            __DIR__ . '/../Config/rewardpoints.php', 'rewardpoints'
+        );
+        
+        $this->app->singleton('cartrewardpoints', function ($app) {
+            return new CartRewardPoints();
+        });
+        
+        $this->app->register(EventServiceProvider::class);
+        
+        $this->app->alias('cartrewardpoints', CartRewardPoints::class);
+        
+        if ($this->app->runningInConsole()) {
+            $this->commands([
+                \Longyi\RewardPoints\Console\Commands\CheckExpiredPoints::class,
+                \Longyi\RewardPoints\Console\Commands\InitializeSettings::class,
+            ]);
+        }
+    }
+     /**
+     * 从数据库加载配置
+     */
+    protected function loadDatabaseSettings()
+    {
+        try {
+            $settingRepository = app(\Longyi\RewardPoints\Repositories\RewardPointSettingRepository::class);
+            
+            // 如果数据库中没有配置,初始化默认值
+            if (DB::table('mw_reward_points_settings')->count() === 0) {
+                $settingRepository->initializeDefaultSettings();
+            }
+            
+            // 获取所有配置
+            $settings = DB::table('mw_reward_points_settings')
+                ->where('is_enabled', true)
+                ->get();
+            
+            foreach ($settings as $setting) {
+                $this->overrideConfig($setting->code, $setting->value, $setting->type);
+            }
+            
+        } catch (\Exception $e) {
+            \Log::error('Load reward points settings from database failed: ' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 覆盖配置文件
+     */
+    protected function overrideConfig($code, $value, $type)
+    {
+        $configKey = $this->codeToConfigKey($code);
+        
+        if ($configKey && config()->has($configKey)) {
+            $typedValue = $this->castValue($value, $type);
+            config()->set($configKey, $typedValue);
+        }
+    }
+    
+    /**
+     * 将代码转换为配置键
+     */
+    protected function codeToConfigKey($code)
+    {
+        $mapping = [
+            'reward_enabled' => 'rewardpoints.general.enabled',
+            'point_value' => 'rewardpoints.general.point_value',
+            'max_discount_percentage' => 'rewardpoints.general.max_discount_percentage',
+            'min_points_to_redeem' => 'rewardpoints.general.min_points_to_redeem',
+            'signin_base_points' => 'rewardpoints.sign_in.base_points',
+            'signin_week_bonus' => 'rewardpoints.sign_in.week_bonus_points',
+            'signin_month_bonus' => 'rewardpoints.sign_in.month_bonus_points',
+            'order_points_per_currency' => 'rewardpoints.order.points_per_currency',
+            'order_min_amount' => 'rewardpoints.order.min_order_amount',
+            'registration_points' => 'rewardpoints.registration.points_per_registration',
+            'review_points' => 'rewardpoints.review.points_per_review',
+            'review_require_approval' => 'rewardpoints.review.require_approval',
+            'referral_points' => 'rewardpoints.referral.points_per_referral',
+            'referral_require_order' => 'rewardpoints.referral.require_first_order',
+            'birthday_points' => 'rewardpoints.birthday.points_per_birthday',
+        ];
+        
+        return $mapping[$code] ?? null;
+    }
+    
+    /**
+     * 类型转换
+     */
+    protected function castValue($value, $type)
+    {
+        if ($value === null) {
+            return null;
+        }
+        
+        switch ($type) {
+            case 'boolean':
+                return (bool) $value;
+            case 'number':
+                return (float) $value;
+            default:
+                return $value;
+        }
+    }
+    
+}

+ 313 - 0
packages/Longyi/RewardPoints/src/Repositories/RewardPointRepository.php

@@ -0,0 +1,313 @@
+<?php
+
+namespace Longyi\RewardPoints\Repositories;
+
+use Webkul\Core\Eloquent\Repository;
+use Longyi\RewardPoints\Models\RewardPointHistory;
+use Longyi\RewardPoints\Models\RewardPointCustomer;
+use Longyi\RewardPoints\Models\RewardActiveRule;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class RewardPointRepository extends Repository
+{
+    public function model()
+    {
+        return RewardPointHistory::class;
+    }
+
+    public function getCustomerPoints($customerId)
+    {
+        $customerPoints = RewardPointCustomer::where('customer_id', $customerId)->first();
+
+        if (!$customerPoints) {
+            return 0;
+        }
+
+        return $customerPoints->mw_reward_point;
+    }
+
+    /**
+     * 添加积分(支持传入规则信息)
+     */
+    public function addPoints($customerId, $type, $amount, $orderId = null, $detail = null, $rule = null)
+    {
+        try {
+            return DB::transaction(function () use ($customerId, $type, $amount, $orderId, $detail, $rule) {
+                // 1. 获取或创建用户积分记录
+                $customerPoints = RewardPointCustomer::firstOrCreate(
+                    ['customer_id' => $customerId],
+                    [
+                        'mw_reward_point' => 0,
+                        'mw_friend_id' => 0,
+                        'subscribed_balance_update' => 1,
+                        'subscribed_point_expiration' => 1,
+                        'last_checkout' => Carbon::now()
+                    ]
+                );
+                
+                // 2. 使用 increment 方法增加积分
+                $customerPoints->increment('mw_reward_point', $amount);
+                
+                // 3. 更新最后检查时间
+                $customerPoints->last_checkout = Carbon::now();
+                $customerPoints->save();
+                
+                // 4. 重新查询获取最新的余额
+                $customerPoints = RewardPointCustomer::where('customer_id', $customerId)->first();
+                $newBalance = $customerPoints ? (int) $customerPoints->mw_reward_point : $amount;
+
+                Log::info('Points added', [
+                    'customer_id' => $customerId,
+                    'type' => $type,
+                    'amount' => $amount,
+                    'new_balance' => $newBalance
+                ]);
+
+                // 5. 获取过期信息(如果没有传入规则,则查询)
+                $expiredDay = 0;
+                $expiredTime = null;
+                
+                if ($rule) {
+                    $expiredDay = $rule->expired_day ?? 0;
+                    $expiredTime = $expiredDay > 0 ? Carbon::now()->addDays($expiredDay) : null;
+                } else {
+                    // 兼容旧代码,如果没有传入规则则查询
+                    $rule = RewardActiveRule::where('type_of_transaction', $type)
+                        ->where('status', 1)
+                        ->first();
+                    $expiredDay = $rule ? $rule->expired_day : 0;
+                    $expiredTime = $expiredDay > 0 ? Carbon::now()->addDays($expiredDay) : null;
+                }
+
+                // 6. 创建历史记录
+                $history = $this->create([
+                    'customer_id' => $customerId,
+                    'type_of_transaction' => $type,
+                    'amount' => $amount,
+                    'balance' => $newBalance,
+                    'transaction_detail' => $detail,
+                    'transaction_time' => Carbon::now(),
+                    'history_order_id' => $orderId ?? 0,
+                    'expired_day' => $expiredDay,
+                    'expired_time' => $expiredTime,
+                    'point_remaining' => $amount,
+                    'check_time' => 1,
+                    'status' => RewardPointHistory::STATUS_COMPLETED
+                ]);
+
+                return $history;
+            });
+        } catch (\Exception $e) {
+            Log::error('Error adding points', [
+                'customer_id' => $customerId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * 批量添加积分(用于同时多个事件)
+     * 确保每条历史记录都有正确的余额值
+     */
+    public function addPointsBatch($customerId, array $pointsList)
+    {
+        try {
+            return DB::transaction(function () use ($customerId, $pointsList) {
+                // 1. 获取或创建用户积分记录(不使用 lockForUpdate 避免问题)
+                $customerPoints = RewardPointCustomer::firstOrCreate(
+                    ['customer_id' => $customerId],
+                    [
+                        'mw_reward_point' => 0,
+                        'mw_friend_id' => 0,
+                        'subscribed_balance_update' => 1,
+                        'subscribed_point_expiration' => 1,
+                        'last_checkout' => Carbon::now()
+                    ]
+                );
+                
+                // 确保 $customerPoints 存在
+                if (!$customerPoints) {
+                    throw new \Exception('Failed to create or retrieve customer points record');
+                }
+                
+                // 2. 获取当前余额
+                $currentBalance = (int) $customerPoints->mw_reward_point;
+                $histories = [];
+                $totalAmount = 0;                                
+                
+                // 3. 逐条创建历史记录(每条记录使用累加后的余额)
+                foreach ($pointsList as $index => $pointData) {
+                    $amount = (int) $pointData['amount'];
+                    $type = $pointData['type'];
+                    $detail = $pointData['detail'] ?? null;
+                    $orderId = $pointData['order_id'] ?? null;
+                    $rule = $pointData['rule'] ?? null;
+                    
+                    // 累加总积分
+                    $totalAmount += $amount;
+                    
+                    // 计算当前这条记录后的余额(累加后的余额)
+                    $newBalance = $currentBalance + $totalAmount;
+                    
+                    // 获取过期信息
+                    $expiredDay = 0;
+                    $expiredTime = null;
+                    
+                    if ($rule) {
+                        $expiredDay = $rule->expired_day ?? 0;
+                        $expiredTime = $expiredDay > 0 ? Carbon::now()->addDays($expiredDay) : null;
+                    }
+                    
+                    // 创建历史记录(每条记录独立)
+                    $history = $this->create([
+                        'customer_id' => $customerId,
+                        'type_of_transaction' => $type,
+                        'amount' => $amount,
+                        'balance' => $newBalance,
+                        'transaction_detail' => $detail,
+                        'transaction_time' => Carbon::now(),
+                        'history_order_id' => $orderId ?? 0,
+                        'expired_day' => $expiredDay,
+                        'expired_time' => $expiredTime,
+                        'point_remaining' => $amount,
+                        'check_time' => 1,
+                        'status' => RewardPointHistory::STATUS_COMPLETED
+                    ]);
+                    
+                    $histories[] = $history;
+        
+                }
+                
+                 // 4. 最后一次性更新用户总积分
+                $updated = DB::table('mw_reward_point_customer')
+                        ->where('customer_id', $customerId)
+                        ->update([
+                            'mw_reward_point' => DB::raw('mw_reward_point + ' . (float)$totalAmount),
+                            'last_checkout' => Carbon::now()
+                        ]);
+                                              
+                return $histories;
+            });
+        } catch (\Exception $e) {
+            Log::error('Error adding batch points', [
+                'customer_id' => $customerId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+            throw $e;
+        }
+    }
+
+    public function deductPoints($customerId, $amount, $orderId = null, $detail = null)
+    {
+        try {
+            return DB::transaction(function () use ($customerId, $amount, $orderId, $detail) {
+                $customerPoints = RewardPointCustomer::where('customer_id', $customerId)
+                    ->lockForUpdate()
+                    ->first();
+
+                if (!$customerPoints) {
+                    Log::warning('Customer points record not found', ['customer_id' => $customerId]);
+                    return false;
+                }
+                
+                if ($customerPoints->mw_reward_point < $amount) {
+                    Log::warning('Insufficient points', [
+                        'customer_id' => $customerId,
+                        'current' => $customerPoints->mw_reward_point,
+                        'required' => $amount
+                    ]);
+                    return false;
+                }
+
+                $currentBalance = (int) $customerPoints->mw_reward_point;
+                $newBalance = $currentBalance - (int) $amount;
+
+                // 更新积分
+                $customerPoints->mw_reward_point = $newBalance;
+                $customerPoints->last_checkout = Carbon::now();
+                $customerPoints->save();
+
+                // 创建历史记录
+                $history = $this->create([
+                    'customer_id' => $customerId,
+                    'type_of_transaction' => 0,
+                    'amount' => -$amount,
+                    'balance' => $newBalance,
+                    'transaction_detail' => $detail,
+                    'transaction_time' => Carbon::now(),
+                    'history_order_id' => $orderId ?? 0,
+                    'expired_day' => 0,
+                    'expired_time' => null,
+                    'point_remaining' => 0,
+                    'check_time' => 1,
+                    'status' => RewardPointHistory::STATUS_COMPLETED
+                ]);
+
+                Log::info('Points deducted successfully', [
+                    'customer_id' => $customerId,
+                    'amount' => $amount,
+                    'new_balance' => $newBalance
+                ]);
+
+                return $history;
+            });
+        } catch (\Exception $e) {
+            Log::error('Error deducting points', [
+                'customer_id' => $customerId,
+                'amount' => $amount,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+
+    public function getHistory($customerId, $limit = 20)
+    {
+        return $this->where('customer_id', $customerId)
+            ->orderBy('transaction_time', 'desc')
+            ->paginate($limit);
+    }
+
+    public function checkExpiredPoints()
+    {
+        $expiredHistories = $this->where('expired_time', '<', Carbon::now())
+            ->where('status', RewardPointHistory::STATUS_COMPLETED)
+            ->where('point_remaining', '>', 0)
+            ->get();
+
+        foreach ($expiredHistories as $history) {
+            try {
+                DB::transaction(function () use ($history) {
+                    $customerPoints = RewardPointCustomer::where('customer_id', $history->customer_id)
+                        ->lockForUpdate()
+                        ->first();
+
+                    if ($customerPoints && $customerPoints->mw_reward_point >= $history->point_remaining) {
+                        $customerPoints->mw_reward_point -= $history->point_remaining;
+                        $customerPoints->save();
+
+                        $history->status = RewardPointHistory::STATUS_EXPIRED;
+                        $history->point_remaining = 0;
+                        $history->save();
+                        
+                        Log::info('Expired points processed', [
+                            'history_id' => $history->history_id,
+                            'customer_id' => $history->customer_id,
+                            'points' => $history->point_remaining
+                        ]);
+                    }
+                });
+            } catch (\Exception $e) {
+                Log::error('Error processing expired points', [
+                    'history_id' => $history->history_id,
+                    'error' => $e->getMessage()
+                ]);
+            }
+        }
+    }
+}

+ 171 - 0
packages/Longyi/RewardPoints/src/Repositories/RewardPointSettingRepository.php

@@ -0,0 +1,171 @@
+<?php
+
+namespace Longyi\RewardPoints\Repositories;
+
+use Webkul\Core\Eloquent\Repository;
+use Longyi\RewardPoints\Models\RewardPointSetting;
+
+class RewardPointSettingRepository extends Repository
+{
+    public function model()
+    {
+        return RewardPointSetting::class;
+    }
+    
+    /**
+     * 获取所有配置分组
+     */
+    public function getGroups()
+    {
+        return $this->model
+            ->select('group')
+            ->distinct()
+            ->pluck('group')
+            ->toArray();
+    }
+    
+    /**
+     * 按分组获取配置
+     */
+    public function getSettingsByGroup($group)
+    {
+        return $this->model
+            ->where('group', $group)
+            ->where('is_enabled', true)
+            ->orderBy('sort_order')
+            ->get();
+    }
+    
+    /**
+     * 获取配置值
+     */
+    public function getConfigValue($code, $default = null)
+    {
+        $setting = $this->findOneByField('code', $code);
+        
+        if (!$setting) {
+            return $default;
+        }
+        
+        return $this->castValue($setting->value, $setting->type);
+    }
+    
+    /**
+     * 批量获取配置值
+     */
+    public function getConfigValues($codes)
+    {
+        $settings = $this->model
+            ->whereIn('code', $codes)
+            ->where('is_enabled', true)
+            ->get();
+        
+        $result = [];
+        foreach ($settings as $setting) {
+            $result[$setting->code] = $this->castValue($setting->value, $setting->type);
+        }
+        
+        return $result;
+    }
+    
+    /**
+     * 设置配置值
+     */
+    public function setConfigValue($code, $value)
+    {
+        return $this->updateOrCreate(
+            ['code' => $code],
+            ['value' => $value]
+        );
+    }
+    
+    /**
+     * 批量设置配置值
+     */
+    public function setConfigValues($values)
+    {
+        foreach ($values as $code => $value) {
+            $this->setConfigValue($code, $value);
+        }
+    }
+    
+    /**
+     * 类型转换
+     */
+    protected function castValue($value, $type)
+    {
+        if ($value === null) {
+            return null;
+        }
+        
+        switch ($type) {
+            case 'boolean':
+                return (bool) $value;
+            case 'number':
+                return (float) $value;
+            default:
+                return $value;
+        }
+    }
+    
+    /**
+     * 初始化默认配置
+     */
+    public function initializeDefaultSettings()
+    {
+        $defaultSettings = $this->getDefaultSettings();
+        
+        foreach ($defaultSettings as $setting) {
+            $this->model->firstOrCreate(
+                ['code' => $setting['code']],
+                $setting
+            );
+        }
+    }
+    
+    /**
+     * 获取默认配置列表
+     */
+    protected function getDefaultSettings()
+    {
+        return [
+            // 通用设置
+            [
+                'code' => 'reward_enabled',
+                'name' => '启用积分系统',
+                'value' => '1',
+                'type' => 'boolean',
+                'group' => 'general',
+                'sort_order' => 1,
+                'description' => '是否启用积分奖励系统'
+            ],
+            [
+                'code' => 'point_value',
+                'name' => '积分价值',
+                'value' => '0.01',
+                'type' => 'number',
+                'group' => 'general',
+                'sort_order' => 2,
+                'description' => '每个积分的价值(元)'
+            ],
+            [
+                'code' => 'max_discount_percentage',
+                'name' => '最大抵扣比例',
+                'value' => '100',
+                'type' => 'number',
+                'group' => 'general',
+                'sort_order' => 3,
+                'description' => '订单最大可抵扣百分比(%)'
+            ],
+            [
+                'code' => 'min_points_to_redeem',
+                'name' => '最低使用积分',
+                'value' => '100',
+                'type' => 'number',
+                'group' => 'general',
+                'sort_order' => 4,
+                'description' => '最低可使用的积分数量'
+            ],
+        ];
+    }
+}

+ 156 - 0
packages/Longyi/RewardPoints/src/Resources/lang/en/rewardpoints.php

@@ -0,0 +1,156 @@
+<?php
+
+return [
+    'customer' => [
+        'my-reward-points' => 'My Reward Points',
+        'available-points' => 'Available Points',
+        'daily-sign-in' => 'Daily Sign In',
+        'sign-in-now' => 'Sign In Now',
+        'signed-today' => 'Signed In Today',
+        'sign-in-success' => 'Sign in successful! You earned',
+        'sign-in-failed' => 'Failed to sign in',
+        'sign-in-error' => 'An error occurred during sign in',
+        'current-streak' => 'Current Streak',
+        'days' => 'days',
+        'points-history' => 'Points History',
+        'date' => 'Date',
+        'type' => 'Type',
+        'description' => 'Description',
+        'points' => 'Points',
+        'balance' => 'Balance',
+        'status' => 'Status',
+        'order' => 'Order',
+        'registration' => 'Registration',
+        'review' => 'Review',
+        'sign-in' => 'Sign In',
+        'referral' => 'Referral',
+        'birthday' => 'Birthday',
+        'other' => 'Other',
+        'pending' => 'Pending',
+        'completed' => 'Completed',
+        'cancelled' => 'Cancelled',
+        'expired' => 'Expired',
+        'no-history' => 'No points history yet',
+    ],
+
+    'admin' => [
+        'reward-points' => 'Reward Points',
+        'rules' => 'Rules',
+        'customers' => 'Customers',
+        'reports' => 'Reports',
+        'add-rule' => 'Add Rule',
+        'transactions' => 'Transactions',
+        'earned-transactions' => 'Earned Transactions',
+        'view-all-transactions' => 'All Transactions',
+        'view-earned-only' => 'Earned Only',
+        'total' => 'Total',
+        'edit-rule' => 'Edit Rule',
+        'delete-rule' => 'Delete Rule',
+        'rule-name' => 'Rule Name',
+        'transaction-type' => 'Transaction Type',
+        'status' => 'Status',
+        'active' => 'Active',
+        'inactive' => 'Inactive',
+        'actions' => 'Actions',
+        'edit' => 'Edit',
+        'delete' => 'Delete',
+        'create-rule' => 'Create Rule',
+        'update-rule' => 'Update Rule',
+        'back' => 'Back',
+        'save' => 'Save',
+        'cancel' => 'Cancel',
+        'customer-list' => 'Customer List',
+        'customer-name' => 'Customer Name',
+        'email' => 'Email',
+        'points-balance' => 'Points Balance',
+        'add-points' => 'Add Points',
+        'deduct-points' => 'Deduct Points',
+        'reason' => 'Reason',
+        'total-points' => 'Total Points',
+        'total-customers' => 'Total Customers',
+        'average-points' => 'Average Points',
+        'export' => 'Export',
+        'bulk-update' => 'Bulk Update',
+        'date-range' => 'Date Range',
+        'start-date' => 'Start Date',
+        'end-date' => 'End Date',
+        'points-earned' => 'Points Earned',
+        'points-redeemed' => 'Points Redeemed',
+        'title' => 'Customer Reward Points',
+        'details-title' => ':name\'s Reward Points Details',
+        'customer-info' => 'Customer Information',
+        'points-history' => 'Points History',
+        'adjust-points' => 'Adjust Points',
+        'type' => 'Type',
+        'amount' => 'Amount',
+        'balance' => 'Balance',
+        'date' => 'Date',
+        'description' => 'Description',
+        'customer-id' => 'Customer ID',
+        'name' => 'Name',
+        'view' => 'View',
+        'no-customers-found' => 'No customers found',
+        'no-reward-points-records' => 'No reward points records available',
+
+        // 交易类型映射(使用字符串而不是数组)
+        'transaction-types' => [
+            'order' => 'Order',
+            'registration' => 'Registration',
+            'review' => 'Product Review',
+            'sign-in' => 'Daily Sign In',
+            'referral' => 'Referral',
+            'birthday' => 'Birthday',
+            'share' => 'Share',
+            'subscribe' => 'Subscribe',
+        ],
+
+        'adjust-points-by-identifier' => 'Adjust Points by Email or ID',
+        'identifier' => 'Identifier',
+        'email-or-id' => 'Email or ID',
+        'enter-email-or-id' => 'Enter customer email or ID',
+        'enter-points' => 'Enter points amount',
+        'enter-reason' => 'Enter reason for adjustment',
+        'submit' => 'Submit',
+        'action' => 'Action',
+
+        // 总交易数
+        'total-transactions' => 'Total Transactions',
+        'total-points-earned' => 'Total Points Earned',
+        'total-points-redeemed' => 'Total Points Redeemed',
+
+        // 搜索相关
+        'search' => 'Search',
+        'reset' => 'Reset',
+        'customer-email' => 'Customer Email',
+        'amount-type' => 'Amount Type',
+        'earned' => 'Earned',
+        'redeemed' => 'Redeemed',
+        'all' => 'All',
+        'all-types' => 'All Types',
+        'admin-adjustment' => 'Admin Adjustment',
+        'admin-action' => 'Admin Action',
+
+        // 表格表头
+        'id' => 'ID',
+        'customer' => 'Customer',
+        'amount' => 'Amount',
+        'date' => 'Date',
+        'description' => 'Description',
+        'balance' => 'Balance',
+
+        // 提示信息
+        'no-transactions-found' => 'No transactions found',
+        'try-adjusting-filters' => 'Try adjusting your filters',
+
+        // 其他
+        'points' => 'Points',
+        'transactions-count' => 'Transactions',
+    ],
+
+    'validation' => [
+        'points-required' => 'Points are required',
+        'points-invalid' => 'Invalid points amount',
+        'customer-not-found' => 'Customer not found',
+        'rule-not-found' => 'Rule not found',
+    ],
+];

+ 105 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/customers/adjust-points.blade.php

@@ -0,0 +1,105 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('rewardpoints::rewardpoints.admin.adjust-points-by-identifier')
+    </x-slot:title>
+
+    <div class="flex gap-4 justify-between items-center mb-4">
+        <p class="text-xl text-gray-800 dark:text-white font-bold">
+            @lang('rewardpoints::rewardpoints.admin.adjust-points-by-identifier')
+        </p>
+    </div>
+
+    <div class="bg-white dark:bg-gray-900 rounded box-shadow">
+        <form method="POST" action="{{ route('admin.reward-points.customers.adjust-points.submit') }}" class="p-4">
+            @csrf
+            <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
+                <div class="mb-4">
+                    <label for="identifier" class="block text-gray-700 dark:text-gray-300 mb-2">
+                        @lang('rewardpoints::rewardpoints.admin.identifier') (@lang('rewardpoints::rewardpoints.admin.email-or-id'))
+                    </label>
+                    <input 
+                        type="text" 
+                        id="identifier" 
+                        name="identifier" 
+                        value="{{ old('identifier') }}"
+                        class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
+                        placeholder="@lang('rewardpoints::rewardpoints.admin.enter-email-or-id')"
+                        required
+                    >
+                    @error('identifier')
+                        <div class="text-red-500 mt-1 text-sm">{{ $message }}</div>
+                    @enderror
+                </div>
+
+                <div class="mb-4">
+                    <label for="action" class="block text-gray-700 dark:text-gray-300 mb-2">
+                        @lang('rewardpoints::rewardpoints.admin.action')
+                    </label>
+                    <select 
+                        id="action" 
+                        name="action" 
+                        class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
+                        required
+                    >
+                        <option value="add">@lang('rewardpoints::rewardpoints.admin.add-points')</option>
+                        <option value="deduct">@lang('rewardpoints::rewardpoints.admin.deduct-points')</option>
+                    </select>
+                    @error('action')
+                        <div class="text-red-500 mt-1 text-sm">{{ $message }}</div>
+                    @enderror
+                </div>
+
+                <div class="mb-4">
+                    <label for="points" class="block text-gray-700 dark:text-gray-300 mb-2">
+                        @lang('rewardpoints::rewardpoints.admin.points')
+                    </label>
+                    <input 
+                        type="number" 
+                        id="points" 
+                        name="points" 
+                        value="{{ old('points') }}"
+                        min="1"
+                        class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
+                        placeholder="@lang('rewardpoints::rewardpoints.admin.enter-points')"
+                        required
+                    >
+                    @error('points')
+                        <div class="text-red-500 mt-1 text-sm">{{ $message }}</div>
+                    @enderror
+                </div>
+
+                <div class="mb-4">
+                    <label for="reason" class="block text-gray-700 dark:text-gray-300 mb-2">
+                        @lang('rewardpoints::rewardpoints.admin.reason')
+                    </label>
+                    <input 
+                        type="text" 
+                        id="reason" 
+                        name="reason" 
+                        value="{{ old('reason', 'Admin adjustment') }}"
+                        class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
+                        placeholder="@lang('rewardpoints::rewardpoints.admin.enter-reason')"
+                    >
+                    @error('reason')
+                        <div class="text-red-500 mt-1 text-sm">{{ $message }}</div>
+                    @enderror
+                </div>
+            </div>
+
+            <div class="mt-6">
+                <button 
+                    type="submit" 
+                    class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
+                >
+                    @lang('rewardpoints::rewardpoints.admin.submit')
+                </button>
+                <a 
+                    href="{{ route('admin.reward-points.customers.index') }}"
+                    class="px-4 py-2 ml-2 bg-gray-500 text-white rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
+                >
+                    @lang('rewardpoints::rewardpoints.admin.cancel')
+                </a>
+            </div>
+        </form>
+    </div>
+</x-admin::layouts>

+ 223 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/customers/index.blade.php

@@ -0,0 +1,223 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('rewardpoints::rewardpoints.admin.title')
+    </x-slot:title>
+
+    {{-- 页面标题和操作按钮 --}}
+    <div class="flex flex-wrap gap-4 justify-between items-center mb-6">
+        <div class="flex items-center gap-3">
+            <div class="icon-users text-2xl text-blue-500"></div>
+            <p class="text-2xl text-gray-800 dark:text-white font-bold">
+                @lang('rewardpoints::rewardpoints.admin.customers')
+            </p>
+            <span class="px-2.5 py-0.5 text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full">
+                {{ $totalCustomers }} @lang('rewardpoints::rewardpoints.admin.total-customers')
+            </span>
+        </div>
+
+        <div class="flex gap-x-2.5 items-center">
+            {{-- 调整积分按钮 --}}
+            <a
+                href="{{ route('admin.reward-points.customers.adjust-form') }}"
+                style="padding: 0.5rem 1rem; background-color: #10b981; color: white; font-weight: 500; border-radius: 0.5rem; text-decoration: none; transition: all 0.15s ease;"
+                onmouseover="this.style.backgroundColor='#059669'"
+                onmouseout="this.style.backgroundColor='#10b981'"
+            >
+                + Adjust Points
+            </a>
+
+            {{-- 导出按钮 --}}
+            <a
+                href="{{ route('admin.reward-points.customers.export') }}"
+                class="secondary-button"
+            >
+                Export
+            </a>
+        </div>
+    </div>
+    {{-- 筛选查询条件 --}}
+    <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 p-4 mb-6">
+        <form method="GET" action="{{ route('admin.reward-points.customers.index') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4">
+            {{-- 邮箱查询 --}}
+            <div>
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                    @lang('rewardpoints::rewardpoints.admin.email')
+                </label>
+                <input
+                    type="text"
+                    name="email"
+                    value="{{ request('email') }}"
+                    placeholder="@lang('Search by email')"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+            </div>
+
+            {{-- 积分最小值 --}}
+            <div>
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                    @lang('Min Points')
+                </label>
+                <input
+                    type="number"
+                    name="min_points"
+                    value="{{ request('min_points') }}"
+                    placeholder="0"
+                    min="0"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+            </div>
+
+            {{-- 积分最大值 --}}
+            <div>
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                    @lang('Max Points')
+                </label>
+                <input
+                    type="number"
+                    name="max_points"
+                    value="{{ request('max_points') }}"
+                    placeholder="10000"
+                    min="0"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+            </div>
+
+            {{-- 操作按钮 --}}
+            <div class="flex items-end gap-2">
+                <button
+                    type="submit"
+                    class="primary-button flex-1"
+                >
+                    <span class="icon-search text-lg"></span>
+                    @lang('Search')
+                </button>
+
+                <a
+                    href="{{ route('admin.reward-points.customers.index') }}"
+                    class="secondary-button"
+                >
+                    <span class="icon-reset text-lg"></span>
+                    @lang('Reset')
+                </a>
+            </div>
+        </form>
+    </div>
+    {{-- 统计卡片 --}}
+    <div class="flex flex-wrap gap-4 mb-6">
+        <div class="flex-1 min-w-[200px] bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-xl p-5 border border-blue-200 dark:border-blue-800">
+            <div class="flex items-center justify-between">
+                <div>
+                    <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">
+                        @lang('rewardpoints::rewardpoints.admin.total-customers')
+                    </p>
+                    <p class="text-3xl font-bold text-blue-600 dark:text-blue-400">
+                        {{ $totalCustomers }}
+                    </p>
+                </div>
+                <div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-full flex items-center justify-center">
+                    <span class="icon-users text-2xl text-blue-600 dark:text-blue-400"></span>
+                </div>
+            </div>
+        </div>
+
+        <div class="flex-1 min-w-[200px] bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-5 border border-green-200 dark:border-green-800">
+            <div class="flex items-center justify-between">
+                <div>
+                    <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">
+                        @lang('rewardpoints::rewardpoints.admin.total-points')
+                    </p>
+                    <p class="text-3xl font-bold text-green-600 dark:text-green-400">
+                        {{ number_format($totalPoints) }}
+                    </p>
+                </div>
+                <div class="w-12 h-12 bg-green-100 dark:bg-green-900/50 rounded-full flex items-center justify-center">
+                    <span class="icon-star text-2xl text-green-600 dark:text-green-400"></span>
+                </div>
+            </div>
+        </div>
+
+        <div class="flex-1 min-w-[200px] bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-5 border border-purple-200 dark:border-purple-800">
+            <div class="flex items-center justify-between">
+                <div>
+                    <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">
+                        @lang('rewardpoints::rewardpoints.admin.average-points')
+                    </p>
+                    <p class="text-3xl font-bold text-purple-600 dark:text-purple-400">
+                        {{ number_format(round($averagePoints, 2), 2) }}
+                    </p>
+                </div>
+                <div class="w-12 h-12 bg-purple-100 dark:bg-purple-900/50 rounded-full flex items-center justify-center">
+                    <span class="icon-chart-line text-2xl text-purple-600 dark:text-purple-400"></span>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    {{-- 表格头部 --}}
+    <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden">
+        <div class="overflow-x-auto">
+            <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
+                <thead class="bg-gray-50 dark:bg-gray-800">
+                    <tr>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('rewardpoints::rewardpoints.admin.customer-id')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('rewardpoints::rewardpoints.admin.name')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('rewardpoints::rewardpoints.admin.email')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('rewardpoints::rewardpoints.admin.reward-points')
+                        </th>
+                        <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('rewardpoints::rewardpoints.admin.actions')
+                        </th>
+                    </tr>
+                </thead>
+                <tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
+                    @forelse($customers as $customer)
+                        <tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                #{{ $customer->customer_id }}
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                {{ $customer->customer->first_name ?? 'N/A' }} {{ $customer->customer->last_name ?? '' }}
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
+                                {{ $customer->customer->email ?? 'N/A' }}
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap">
+                                <div class="flex items-center gap-1">
+                                    <span class="text-xl font-bold text-yellow-600 dark:text-yellow-500">
+                                        {{ number_format($customer->mw_reward_point) }}
+                                    </span>
+                                    <span class="text-xs text-gray-500">pts</span>
+                                </div>
+                            </td>
+                        </tr>
+                    @empty
+                        <tr>
+                            <td colspan="5" class="px-6 py-12 text-center">
+                                <div class="flex flex-col items-center gap-4">
+                                    <div class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
+                                        <span class="icon-users text-4xl text-gray-400"></span>
+                                    </div>
+                                    <div>
+                                        <p class="text-lg font-medium text-gray-600 dark:text-gray-400 mb-1">
+                                            @lang('rewardpoints::rewardpoints.admin.no-customers-found')
+                                        </p>
+                                        <p class="text-sm text-gray-500 dark:text-gray-500">
+                                            @lang('rewardpoints::rewardpoints.admin.no-reward-points-records')
+                                        </p>
+                                    </div>
+                                </div>
+                            </td>
+                        </tr>
+                    @endforelse
+                </tbody>
+            </table>
+        </div>
+    </div>
+</x-admin::layouts>

+ 121 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/customers/show.blade.php

@@ -0,0 +1,121 @@
+@extends('admin::layouts.content')
+
+@section('page_title')
+    {{ __('rewardpoints::app.admin.customers.details-title', ['name' => $customer->first_name . ' ' . $customer->last_name]) }}
+@stop
+
+@section('content')
+    <div class="content">
+        <div class="page-header">
+            <div class="page-title">
+                <h1>{{ $customer->first_name . ' ' . $customer->last_name }}</h1>
+            </div>
+        </div>
+
+        <div class="page-content">
+            <accordian :title="'{{ __('rewardpoints::app.admin.customers.customer-info') }}'" :active="true">
+                <div slot="body">
+                    <div class="form-container">
+                        <div class="control-group">
+                            <label>{{ __('rewardpoints::app.admin.customers.name') }}</label>
+                            <input type="text" class="control" value="{{ $customer->first_name . ' ' . $customer->last_name }}" readonly>
+                        </div>
+
+                        <div class="control-group">
+                            <label>{{ __('rewardpoints::app.admin.customers.email') }}</label>
+                            <input type="text" class="control" value="{{ $customer->email }}" readonly>
+                        </div>
+
+                        <div class="control-group">
+                            <label>{{ __('rewardpoints::app.admin.customers.reward-points') }}</label>
+                            <input type="text" class="control" value="{{ $rewardData->mw_reward_point }}" readonly>
+                        </div>
+                    </div>
+                </div>
+            </accordian>
+
+            <accordian :title="'{{ __('rewardpoints::app.admin.customers.points-history') }}'" :active="true">
+                <div slot="body">
+                    <div class="table">
+                        <table>
+                            <thead>
+                                <tr>
+                                    <th>{{ __('rewardpoints::app.admin.customers.type') }}</th>
+                                    <th>{{ __('rewardpoints::app.admin.customers.amount') }}</th>
+                                    <th>{{ __('rewardpoints::app.admin.customers.balance') }}</th>
+                                    <th>{{ __('rewardpoints::app.admin.customers.date') }}</th>
+                                    <th>{{ __('rewardpoints::app.admin.customers.description') }}</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                @foreach ($history as $record)
+                                <tr>
+                                    <td>{{ $record->type_of_transaction }}</td>
+                                    <td>{{ $record->amount }}</td>
+                                    <td>{{ $record->balance }}</td>
+                                    <td>{{ $record->transaction_time }}</td>
+                                    <td>{{ $record->transaction_detail }}</td>
+                                </tr>
+                                @endforeach
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </accordian>
+
+            <accordian :title="'{{ __('rewardpoints::app.admin.customers.adjust-points') }}'" :active="false">
+                <div slot="body">
+                    <form method="POST" action="{{ route('admin.reward-points.customers.add-points') }}">
+                        @csrf
+                        <input type="hidden" name="customer_id" value="{{ $customer->id }}">
+
+                        <div class="form-container">
+                            <div class="control-group">
+                                <label for="points">{{ __('rewardpoints::app.admin.customers.points') }}</label>
+                                <input type="number" class="control" id="points" name="points" required>
+                            </div>
+
+                            <div class="control-group">
+                                <label for="type">{{ __('rewardpoints::app.admin.customers.type') }}</label>
+                                <select class="control" id="type" name="type" required>
+                                    <option value="1">{{ __('rewardpoints::app.admin.transactions.order') }}</option>
+                                    <option value="2">{{ __('rewardpoints::app.admin.transactions.registration') }}</option>
+                                    <option value="3">{{ __('rewardpoints::app.admin.transactions.review') }}</option>
+                                    <option value="4">{{ __('rewardpoints::app.admin.transactions.sign-in') }}</option>
+                                    <option value="5">{{ __('rewardpoints::app.admin.transactions.referral') }}</option>
+                                    <option value="6">{{ __('rewardpoints::app.admin.transactions.birthday') }}</option>
+                                </select>
+                            </div>
+
+                            <div class="control-group">
+                                <label for="reason">{{ __('rewardpoints::app.admin.customers.reason') }}</label>
+                                <input type="text" class="control" id="reason" name="reason">
+                            </div>
+
+                            <button type="submit" class="btn btn-md btn-primary">{{ __('rewardpoints::app.admin.customers.add-points') }}</button>
+                        </div>
+                    </form>
+
+                    <form method="POST" action="{{ route('admin.reward-points.customers.deduct-points') }}" style="margin-top: 20px;">
+                        @csrf
+                        <input type="hidden" name="customer_id" value="{{ $customer->id }}">
+
+                        <div class="form-container">
+                            <div class="control-group">
+                                <label for="deduct_points">{{ __('rewardpoints::app.admin.customers.points') }}</label>
+                                <input type="number" class="control" id="deduct_points" name="points" required>
+                            </div>
+
+                            <div class="control-group">
+                                <label for="deduct_reason">{{ __('rewardpoints::app.admin.customers.reason') }}</label>
+                                <input type="text" class="control" id="deduct_reason" name="reason">
+                            </div>
+
+                            <button type="submit" class="btn btn-md btn-danger">{{ __('rewardpoints::app.admin.customers.deduct-points') }}</button>
+                        </div>
+                    </form>
+                </div>
+            </accordian>
+        </div>
+    </div>
+@endsection

+ 198 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/reports/index.blade.php

@@ -0,0 +1,198 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('Reward Points Reports')
+        </x-slot>
+
+        {{-- 页面标题 --}}
+        <div class="flex flex-wrap gap-4 justify-between items-center mb-6">
+            <div class="flex items-center gap-3">
+                <div class="icon-chart-bar text-2xl text-blue-500"></div>
+                <p class="text-2xl text-gray-800 dark:text-white font-bold">
+                    @lang('Reward Points Reports')
+                </p>
+            </div>
+
+            <div class="flex gap-x-2.5 items-center">
+                <a href="{{ route('admin.reward-points.reports.export', ['start_date' => $startDate, 'end_date' => $endDate]) }}"
+                   class="primary-button flex items-center gap-2">
+                    <span class="icon-download text-lg"></span>
+                    @lang('Export Report')
+                </a>
+            </div>
+        </div>
+
+        {{-- 日期筛选 --}}
+        <div class="mb-6 bg-white dark:bg-gray-900 rounded-lg shadow-sm p-4 border border-gray-200 dark:border-gray-800">
+            <form method="GET" action="{{ route('admin.reward-points.reports.index') }}" class="flex flex-wrap gap-4 items-end">
+                <div>
+                    <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                        @lang('Start Date')
+                    </label>
+                    <input
+                        type="date"
+                        name="start_date"
+                        value="{{ $startDate }}"
+                        class="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-300"
+                    >
+                </div>
+
+                <div>
+                    <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                        @lang('End Date')
+                    </label>
+                    <input
+                        type="date"
+                        name="end_date"
+                        value="{{ $endDate }}"
+                        class="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-300"
+                    >
+                </div>
+
+                <div>
+                    <button type="submit" class="primary-button">
+                        @lang('Filter')
+                    </button>
+                </div>
+            </form>
+        </div>
+
+        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+            {{-- 统计卡片 --}}
+            <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm p-6 border border-gray-200 dark:border-gray-800">
+                <h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">@lang('Summary Statistics')</h3>
+                <div class="space-y-3">
+                    <div class="flex justify-between items-center">
+                        <span class="text-gray-600 dark:text-gray-400">@lang('Total Points Earned'):</span>
+                        <span class="text-2xl font-bold text-green-600 dark:text-green-500">{{ number_format($data['total_points_earned']) }}</span>
+                    </div>
+                    <div class="flex justify-between items-center">
+                        <span class="text-gray-600 dark:text-gray-400">@lang('Points Redeemed'):</span>
+                        <span class="text-2xl font-bold text-red-600 dark:text-red-500">{{ number_format($data['points_redeemed']) }}</span>
+                    </div>
+                    <div class="flex justify-between items-center">
+                        <span class="text-gray-600 dark:text-gray-400">@lang('Total Transactions'):</span>
+                        <span class="text-2xl font-bold text-blue-600 dark:text-blue-500">{{ number_format($data['total_transactions']) }}</span>
+                    </div>
+                    <div class="flex justify-between items-center">
+                        <span class="text-gray-600 dark:text-gray-400">@lang('Customers with Points'):</span>
+                        <span class="text-2xl font-bold text-purple-600 dark:text-purple-500">{{ number_format($data['total_customers']) }}</span>
+                    </div>
+                </div>
+            </div>
+
+            {{-- 积分类型分布 --}}
+            <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm p-6 border border-gray-200 dark:border-gray-800">
+                <h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">@lang('Points by Type')</h3>
+                <div class="space-y-3">
+                    @php
+                        $typeNames = [
+                            1 => 'Order',
+                            2 => 'Registration',
+                            3 => 'Product Review',
+                            4 => 'Daily Sign In',
+                            5 => 'Referral',
+                            6 => 'Birthday',
+                            8 => 'Subscribe',
+                            0 => 'Admin Adjustment'
+                        ];
+                    @endphp
+
+                    @forelse($data['points_by_type'] as $item)
+                        <div class="flex justify-between items-center">
+                            <div>
+                                <span class="text-gray-700 dark:text-gray-300">{{ $typeNames[$item->type_of_transaction] ?? 'Unknown' }}</span>
+                                <span class="text-xs text-gray-500 ml-2">({{ number_format($item->total_transactions) }} @lang('transactions'))</span>
+                            </div>
+                            <span class="font-semibold text-gray-800 dark:text-white">{{ number_format($item->total_points) }}</span>
+                        </div>
+                    @empty
+                        <p class="text-gray-500 dark:text-gray-400 text-center py-4">@lang('No data available')</p>
+                    @endforelse
+                </div>
+            </div>
+        </div>
+
+        {{-- 每日签到统计 --}}
+        <div class="mt-6 bg-white dark:bg-gray-900 rounded-lg shadow-sm p-6 border border-gray-200 dark:border-gray-800">
+            <h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">@lang('Daily Sign-In Statistics')</h3>
+            <div class="overflow-x-auto">
+                <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
+                    <thead class="bg-gray-50 dark:bg-gray-800">
+                    <tr>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">@lang('Date')</th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">@lang('Total Sign-Ins')</th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">@lang('Points Earned')</th>
+                    </tr>
+                    </thead>
+                    <tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
+                    @forelse($data['sign_ins'] as $sign)
+                        <tr>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                {{ \Carbon\Carbon::parse($sign->date)->format('Y-m-d') }}
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                {{ number_format($sign->total_sign_ins) }}
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                {{ number_format($sign->total_points_earned) }}
+                            </td>
+                        </tr>
+                    @empty
+                        <tr>
+                            <td colspan="3" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
+                                @lang('No sign-in data available')
+                            </td>
+                        </tr>
+                    @endforelse
+                    </tbody>
+                </table>
+            </div>
+        </div>
+
+        {{-- 积分排行榜 --}}
+        <div class="mt-6 bg-white dark:bg-gray-900 rounded-lg shadow-sm p-6 border border-gray-200 dark:border-gray-800">
+            <h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">@lang('Top 10 Customers by Points')</h3>
+            <div class="overflow-x-auto">
+                <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
+                    <thead class="bg-gray-50 dark:bg-gray-800">
+                    <tr>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">@lang('Rank')</th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">@lang('Customer')</th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">@lang('Email')</th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">@lang('Points Balance')</th>
+                    </tr>
+                    </thead>
+                    <tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
+                    @forelse($data['top_customers'] as $index => $customer)
+                        <tr>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                #{{ $index + 1 }}
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                @if($customer->customer)
+                                    {{ $customer->customer->first_name }} {{ $customer->customer->last_name }}
+                                @else
+                                    N/A
+                                @endif
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                {{ $customer->customer ? $customer->customer->email : 'N/A' }}
+                            </td>
+                            <td class="px-6 py-4 whitespace-nowrap">
+                                <span class="text-lg font-bold text-yellow-600 dark:text-yellow-500">
+                                    {{ number_format($customer->mw_reward_point) }}
+                                </span>
+                            </td>
+                        </tr>
+                    @empty
+                        <tr>
+                            <td colspan="4" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
+                                @lang('No customer data available')
+                            </td>
+                        </tr>
+                    @endforelse
+                    </tbody>
+                </table>
+            </div>
+        </div>
+</x-admin::layouts>

+ 327 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/rules/create.blade.php

@@ -0,0 +1,327 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('Create Reward Rule')
+    </x-slot>
+
+    <div class="flex gap-4 justify-between items-center mb-4">
+        <p class="text-xl text-gray-800 dark:text-white font-bold">
+            @lang('Create Reward Rule')
+        </p>
+    </div>
+
+    <div class="flex gap-2.5 mt-3.5">
+        <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+            <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                <form method="POST" action="{{ route('admin.reward-points.rules.store') }}" id="ruleForm">
+                    @csrf
+
+                    <div class="grid grid-cols-2 gap-2.5">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 required">
+                                @lang('Rule Name')
+                            </label>
+                            <input
+                                type="text"
+                                name="rule_name"
+                                value="{{ old('rule_name') }}"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                required
+                                placeholder="@lang('E.g.: Sign-In Points')"
+                            >
+                            @error('rule_name')
+                                <p class="mt-1 text-xs text-red-600">
+                                    {{ $message }}
+                                </p>
+                            @enderror
+                        </div>
+
+
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 required">
+                                @lang('Transaction Type')
+                            </label>
+                            <select
+                                name="type_of_transaction"
+                                id="transactionType"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                required
+                            >
+                                <option value="">@lang('Select Type')</option>
+                                @foreach($transactionTypes as $key => $type)
+                                    <option value="{{ $key }}" {{ old('type_of_transaction') == $key ? 'selected' : '' }}>
+                                        @lang($type['name'])
+                                    </option>
+                                @endforeach
+                            </select>
+                            <p class="mt-1 text-xs text-gray-500">
+                                @lang('When Transaction Type is "Order", Reward Points will be multiplied by the actual payment amount (default: 1x)')
+                            </p>
+                            @error('type_of_transaction')
+                            <p class="mt-1 text-xs text-red-600">
+                                {{ $message }}
+                            </p>
+                            @enderror
+                        </div>
+
+
+                    </div>
+
+                    {{-- Store View 字段 - 多选下拉框,包含 All Stores 选项 --}}
+                    <div class="mb-2.5">
+                        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                            @lang('Store View')
+                        </label>
+                        <select
+                            name="store_view[]"
+                            id="storeViewSelect"
+                            class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                            multiple
+                        >
+                            <option value="all" {{ in_array('all', old('store_view', [])) ? 'selected' : '' }}>
+                                @lang('All Stores')
+                            </option>
+                            @foreach($storeViews as $key => $value)
+                                <option value="{{ $key }}" {{ in_array($key, old('store_view', [])) ? 'selected' : '' }}>
+                                    {{ $value }}
+                                </option>
+                            @endforeach
+                        </select>
+                        <p class="mt-1 text-xs text-gray-500">
+                            @lang('Select "All Stores" or choose specific stores')
+                        </p>
+                    </div>
+
+                    <!-- 启用不同群组不同积分的开关 -->
+                    <div class="mb-2.5">
+                        <label class="flex items-center gap-2">
+                            <input
+                                type="checkbox"
+                                name="enable_different_points_by_group"
+                                id="enableDifferentPointsByGroup"
+                                value="1"
+                                class="rounded-md border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
+                                {{ old('enable_different_points_by_group') ? 'checked' : '' }}
+                            >
+                            <span class="text-sm font-medium text-gray-700 dark:text-gray-300">
+                                @lang('Enable different points for different customer groups')
+                            </span>
+                        </label>
+                    </div>
+
+                    <!-- 统一积分设置部分 -->
+                    <div id="uniformPointsSection" style="{{ old('enable_different_points_by_group') ? 'display: none;' : '' }}">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Customer Groups')
+                            </label>
+                            <select
+                                name="customer_group_ids[]"
+                                id="customerGroups"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                multiple
+                            >
+                                @foreach($customerGroups as $key => $value)
+                                    <option value="{{ $key }}" {{ in_array($key, old('customer_group_ids', [])) ? 'selected' : '' }}>
+                                        {{ $value }}
+                                    </option>
+                                @endforeach
+                            </select>
+                            <p class="mt-1 text-xs text-gray-500">
+                                @lang('Leave empty for all customer groups')
+                            </p>
+                        </div>
+
+                        <div class="grid grid-cols-2 gap-2.5">
+                            <div class="mb-2.5">
+                                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 required">
+                                    @lang('Reward Points')
+                                </label>
+                                <input
+                                    type="number"
+                                    name="reward_point"
+                                    id="rewardPoints"
+                                    value="{{ old('reward_point', 0) }}"
+                                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                    required
+                                    min="0"
+                                >
+                                @error('reward_point')
+                                    <p class="mt-1 text-xs text-red-600">
+                                        {{ $message }}
+                                    </p>
+                                @enderror
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- 客户群组特定积分设置部分 -->
+                    <div id="groupSpecificPointsSection" style="{{ !old('enable_different_points_by_group') ? 'display: none;' : '' }}">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Customer Group Specific Points')
+                            </label>
+                            <p class="mt-1 text-xs text-gray-500">
+                                @lang('Set different points for each customer group')
+                            </p>
+
+                            <div class="mt-2 space-y-3">
+                                @foreach($customerGroups as $key => $value)
+                                    <div class="flex items-center gap-2">
+                                        <label class="w-32 text-sm text-gray-600 dark:text-gray-300">
+                                            {{ $value }}:
+                                        </label>
+                                        <input
+                                            type="number"
+                                            name="group_points[{{ $key }}]"
+                                            value="{{ old("group_points.{$key}", 0) }}"
+                                            class="flex w-32 min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                            min="0"
+                                            placeholder="0"
+                                        >
+                                    </div>
+                                @endforeach
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="grid grid-cols-2 gap-2.5">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Status')
+                            </label>
+                            <select
+                                name="status"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                            >
+                                <option value="1" {{ old('status', 1) == 1 ? 'selected' : '' }}>@lang('Active')</option>
+                                <option value="0" {{ old('status', 1) == 0 ? 'selected' : '' }}>@lang('Inactive')</option>
+                            </select>
+                        </div>
+                    </div>
+
+                    <div class="grid grid-cols-2 gap-2.5">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Expired Day')
+                            </label>
+                            <input
+                                type="number"
+                                name="expired_day"
+                                value="{{ old('expired_day', 0) }}"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                min="0"
+                            >
+                            <p class="mt-1 text-xs text-gray-500">
+                                @lang('0 means never expires')
+                            </p>
+                        </div>
+                    </div>
+
+                    <div class="mb-2.5">
+                        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                            @lang('Comment')
+                        </label>
+                        <textarea
+                            name="comment"
+                            rows="3"
+                            class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                        >{{ old('comment') }}</textarea>
+                    </div>
+
+                    <div class="flex gap-2.5 items-center">
+                        <button
+                            type="submit"
+                            class="primary-button"
+                        >
+                            @lang('Create Rule')
+                        </button>
+                        <a href="{{ route('admin.reward-points.rules.index') }}" class="secondary-button">
+                            @lang('Back')
+                        </a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+    <style>
+    /* 确保隐藏样式生效 */
+    .hidden {
+        display: none !important;
+    }
+    </style>
+
+   <script>
+    (function() {
+        // Store View 多选逻辑
+        const storeViewSelect = document.getElementById('storeViewSelect');
+
+        if (storeViewSelect) {
+            storeViewSelect.addEventListener('change', function(e) {
+                const selectedOptions = Array.from(this.selectedOptions);
+                const hasAllStores = selectedOptions.some(option => option.value === 'all');
+
+                if (hasAllStores) {
+                    // 如果选中了 "All Stores",清空其他选项,只保留 "All Stores"
+                    Array.from(this.options).forEach(option => {
+                        if (option.value !== 'all') {
+                            option.selected = false;
+                        }
+                    });
+                } else {
+                    // 如果没有选中 "All Stores",确保 "All Stores" 不被选中
+                    const allStoresOption = Array.from(this.options).find(option => option.value === 'all');
+                    if (allStoresOption && allStoresOption.selected) {
+                        allStoresOption.selected = false;
+                    }
+                }
+            });
+        }
+
+        // 定义更新显示的函数
+        function updateSectionsVisibility() {
+            const checkbox = document.getElementById('enableDifferentPointsByGroup');
+            const uniformSection = document.getElementById('uniformPointsSection');
+            const groupSection = document.getElementById('groupSpecificPointsSection');
+
+            if (!checkbox || !uniformSection || !groupSection) {
+                return;
+            }
+
+            if (checkbox.checked) {
+                uniformSection.style.display = 'none';
+                groupSection.style.display = 'block';
+
+                // 当切换到分组模式时,移除统一积分字段的 required 属性
+                const rewardPointsInput = document.getElementById('rewardPoints');
+                if (rewardPointsInput) {
+                    rewardPointsInput.removeAttribute('required');
+                }
+            } else {
+                uniformSection.style.display = 'block';
+                groupSection.style.display = 'none';
+
+                // 当切换到统一模式时,添加 required 属性
+                const rewardPointsInput = document.getElementById('rewardPoints');
+                if (rewardPointsInput) {
+                    rewardPointsInput.setAttribute('required', 'required');
+                }
+            }
+        }
+
+        // 使用事件委托捕获所有 change 事件
+        document.body.addEventListener('change', function(e) {
+            if (e.target && e.target.id === 'enableDifferentPointsByGroup') {
+                updateSectionsVisibility();
+            }
+        });
+
+        // DOM 加载完成后初始化显示状态
+        if (document.readyState === 'loading') {
+            document.addEventListener('DOMContentLoaded', updateSectionsVisibility);
+        } else {
+            updateSectionsVisibility();
+        }
+    })();
+    </script>
+</x-admin::layouts>

+ 325 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/rules/edit.blade.php

@@ -0,0 +1,325 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('Edit Reward Rule')
+    </x-slot>
+
+    <div class="flex gap-4 justify-between items-center mb-4">
+        <p class="text-xl text-gray-800 dark:text-white font-bold">
+            @lang('Edit Reward Rule')
+        </p>
+    </div>
+
+    <div class="flex gap-2.5 mt-3.5">
+        <div class="flex flex-col gap-2 flex-1 max-xl:flex-auto">
+            <div class="p-4 bg-white dark:bg-gray-900 rounded box-shadow">
+                <form method="POST" action="{{ route('admin.reward-points.rules.update', $rule->rule_id) }}" id="ruleForm">
+                    @csrf
+                    @method('PUT')
+
+                    <div class="grid grid-cols-2 gap-2.5">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 required">
+                                @lang('Rule Name')
+                            </label>
+                            <input
+                                type="text"
+                                name="rule_name"
+                                value="{{ old('rule_name', $rule->rule_name) }}"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                required
+                                placeholder="@lang('E.g.: Sign-In Points')"
+                            >
+                            @error('rule_name')
+                                <p class="mt-1 text-xs text-red-600">
+                                    {{ $message }}
+                                </p>
+                            @enderror
+                        </div>
+                        
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 required">
+                                @lang('Transaction Type')
+                            </label>
+                            <select
+                                name="type_of_transaction"
+                                id="transactionType"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                required
+                            >
+                                <option value="">@lang('Select Type')</option>
+                                @foreach($transactionTypes as $key => $type)
+                                    <option value="{{ $key }}" {{ old('type_of_transaction', $rule->type_of_transaction) == $key ? 'selected' : '' }}>
+                                        @lang($type['name'])
+                                    </option>
+                                @endforeach
+                            </select>
+                            @error('type_of_transaction')
+                                <p class="mt-1 text-xs text-red-600">
+                                    {{ $message }}
+                                </p>
+                            @enderror
+                        </div>
+                    </div>
+
+                    {{-- Store View 字段 - 多选下拉框,包含 All Stores 选项 --}}
+                    <div class="mb-2.5">
+                        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                            @lang('Store View')
+                        </label>
+                        <select
+                            name="store_view[]"
+                            id="storeViewSelect"
+                            class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                            multiple
+                        >
+                            <option value="all" {{ in_array('all', old('store_view', $selectedStoreViewsForSelect)) ? 'selected' : '' }}>
+                                @lang('All Stores')
+                            </option>
+                            @foreach($storeViews as $key => $value)
+                                <option value="{{ $key }}" {{ in_array($key, old('store_view', $selectedStoreViewsForSelect)) ? 'selected' : '' }}>
+                                    {{ $value }}
+                                </option>
+                            @endforeach
+                        </select>
+                        <p class="mt-1 text-xs text-gray-500">
+                            @lang('Select "All Stores" or choose specific stores')
+                        </p>
+                    </div>
+
+                    <!-- 启用不同群组不同积分的开关 -->
+                    <div class="mb-2.5">
+                        <label class="flex items-center gap-2">
+                            <input
+                                type="checkbox"
+                                name="enable_different_points_by_group"
+                                id="enableDifferentPointsByGroup"
+                                value="1"
+                                class="rounded-md border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
+                                {{ old('enable_different_points_by_group', $rule->enable_different_points_by_group) ? 'checked' : '' }}
+                            >
+                            <span class="text-sm font-medium text-gray-700 dark:text-gray-300">
+                                @lang('Enable different points for different customer groups')
+                            </span>
+                        </label>
+                    </div>
+
+                    <!-- 统一积分设置部分 -->
+                    <div id="uniformPointsSection" style="{{ old('enable_different_points_by_group', $rule->enable_different_points_by_group) ? 'display: none;' : '' }}">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Customer Groups')
+                            </label>
+                            <select
+                                name="customer_group_ids[]"
+                                id="customerGroups"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                multiple
+                            >
+                                @foreach($customerGroups as $key => $value)
+                                    <option value="{{ $key }}" {{ in_array($key, old('customer_group_ids', $selectedCustomerGroupsArray)) ? 'selected' : '' }}>
+                                        {{ $value }}
+                                    </option>
+                                @endforeach
+                            </select>
+                            <p class="mt-1 text-xs text-gray-500">
+                                @lang('Leave empty for all customer groups')
+                            </p>
+                        </div>
+
+                        <div class="grid grid-cols-2 gap-2.5">
+                            <div class="mb-2.5">
+                                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 required">
+                                    @lang('Reward Points')
+                                </label>
+                                <input
+                                    type="number"
+                                    name="reward_point"
+                                    id="rewardPoints"
+                                    value="{{ old('reward_point', $rule->reward_point) }}"
+                                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                    {{ !old('enable_different_points_by_group', $rule->enable_different_points_by_group) ? 'required' : '' }}
+                                    min="0"
+                                >
+                                @error('reward_point')
+                                    <p class="mt-1 text-xs text-red-600">
+                                        {{ $message }}
+                                    </p>
+                                @enderror
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- 客户群组特定积分设置部分 -->
+                    <div id="groupSpecificPointsSection" style="{{ !old('enable_different_points_by_group', $rule->enable_different_points_by_group) ? 'display: none;' : '' }}">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Customer Group Specific Points')
+                            </label>
+                            <p class="mt-1 text-xs text-gray-500">
+                                @lang('Set different points for each customer group')
+                            </p>
+                            
+                            <div class="mt-2 space-y-3">
+                                @foreach($customerGroups as $key => $value)
+                                    @php
+                                        $pointsValue = isset($selectedCustomerGroupsPoints[$key]) ? $selectedCustomerGroupsPoints[$key] : 0;
+                                        $oldValue = old("group_points.{$key}", $pointsValue);
+                                    @endphp
+                                    <div class="flex items-center gap-2">
+                                        <label class="w-32 text-sm text-gray-600 dark:text-gray-300">
+                                            {{ $value }}:
+                                        </label>
+                                        <input
+                                            type="number"
+                                            name="group_points[{{ $key }}]"
+                                            value="{{ $oldValue }}"
+                                            class="flex w-32 min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                            min="0"
+                                            placeholder="0"
+                                        >
+                                    </div>
+                                @endforeach
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="grid grid-cols-2 gap-2.5">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Status')
+                            </label>
+                            <select
+                                name="status"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                            >
+                                <option value="1" {{ old('status', $rule->status) == 1 ? 'selected' : '' }}>@lang('Active')</option>
+                                <option value="0" {{ old('status', $rule->status) == 0 ? 'selected' : '' }}>@lang('Inactive')</option>
+                            </select>
+                        </div>
+                    </div>
+
+                    <div class="grid grid-cols-2 gap-2.5">
+                        <div class="mb-2.5">
+                            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                                @lang('Expired Day')
+                            </label>
+                            <input
+                                type="number"
+                                name="expired_day"
+                                value="{{ old('expired_day', $rule->expired_day) }}"
+                                class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                                min="0"
+                            >
+                            <p class="mt-1 text-xs text-gray-500">
+                                @lang('0 means never expires')
+                            </p>
+                        </div>
+                    </div>
+    
+                    <div class="mb-2.5">
+                        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+                            @lang('Comment')
+                        </label>
+                        <textarea
+                            name="comment"
+                            rows="3"
+                            class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                        >{{ old('comment', $rule->comment) }}</textarea>
+                    </div>
+
+                    <div class="flex gap-2.5 items-center">
+                        <button 
+                            type="submit"
+                            class="primary-button"
+                        >
+                            @lang('Update Rule')
+                        </button>
+                        <a href="{{ route('admin.reward-points.rules.index') }}" class="secondary-button">
+                            @lang('Back')
+                        </a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+    <style>
+    /* 确保隐藏样式生效 */
+    .hidden {
+        display: none !important;
+    }
+    </style>
+
+   <script>
+    (function() {
+        // Store View 多选逻辑
+        const storeViewSelect = document.getElementById('storeViewSelect');
+        
+        if (storeViewSelect) {
+            storeViewSelect.addEventListener('change', function(e) {
+                const selectedOptions = Array.from(this.selectedOptions);
+                const hasAllStores = selectedOptions.some(option => option.value === 'all');
+                
+                if (hasAllStores) {
+                    // 如果选中了 "All Stores",清空其他选项,只保留 "All Stores"
+                    Array.from(this.options).forEach(option => {
+                        if (option.value !== 'all') {
+                            option.selected = false;
+                        }
+                    });
+                } else {
+                    // 如果没有选中 "All Stores",确保 "All Stores" 不被选中
+                    const allStoresOption = Array.from(this.options).find(option => option.value === 'all');
+                    if (allStoresOption && allStoresOption.selected) {
+                        allStoresOption.selected = false;
+                    }
+                }
+            });
+        }
+        
+        // 定义更新显示的函数
+        function updateSectionsVisibility() {
+            const checkbox = document.getElementById('enableDifferentPointsByGroup');
+            const uniformSection = document.getElementById('uniformPointsSection');
+            const groupSection = document.getElementById('groupSpecificPointsSection');
+            
+            if (!checkbox || !uniformSection || !groupSection) {
+                return;
+            }
+            
+            if (checkbox.checked) {
+                uniformSection.style.display = 'none';
+                groupSection.style.display = 'block';
+                
+                // 当切换到分组模式时,移除统一积分字段的 required 属性
+                const rewardPointsInput = document.getElementById('rewardPoints');
+                if (rewardPointsInput) {
+                    rewardPointsInput.removeAttribute('required');
+                }
+            } else {
+                uniformSection.style.display = 'block';
+                groupSection.style.display = 'none';
+                
+                // 当切换到统一模式时,添加 required 属性
+                const rewardPointsInput = document.getElementById('rewardPoints');
+                if (rewardPointsInput) {
+                    rewardPointsInput.setAttribute('required', 'required');
+                }
+            }
+        }
+        
+        // 监听复选框变化
+        const checkbox = document.getElementById('enableDifferentPointsByGroup');
+        if (checkbox) {
+            checkbox.addEventListener('change', updateSectionsVisibility);
+        }
+        
+        // DOM 加载完成后初始化显示状态
+        if (document.readyState === 'loading') {
+            document.addEventListener('DOMContentLoaded', updateSectionsVisibility);
+        } else {
+            updateSectionsVisibility();
+        }
+    })();
+    </script>
+</x-admin::layouts>

+ 459 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/rules/index.blade.php

@@ -0,0 +1,459 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('Reward Rules')
+    </x-slot>
+
+    {{-- 页面标题和操作按钮 --}}
+    <div class="flex flex-wrap gap-4 justify-between items-center mb-6">
+        <div class="flex items-center gap-3">
+            <div class="icon-star text-2xl text-yellow-500"></div>
+            <p class="text-2xl text-gray-800 dark:text-white font-bold">
+                @lang('Reward Rules')
+            </p>
+            <span class="px-2.5 py-0.5 text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full">
+                {{ $rules->total() }} @lang('Total')
+            </span>
+        </div>
+
+        <div class="flex gap-x-2.5 items-center">
+            <a href="{{ route('admin.reward-points.rules.create') }}" class="primary-button flex items-center gap-2">
+                <span class="icon-plus text-lg"></span>
+                @lang('Add Rule')
+            </a>
+        </div>
+    </div>
+
+    {{-- 搜索和筛选区域 --}}
+    <div class="mb-6 bg-white dark:bg-gray-900 rounded-lg shadow-sm p-4 border border-gray-200 dark:border-gray-800">
+        <div class="flex flex-wrap gap-4 items-center justify-between">
+            <div class="flex flex-wrap gap-3">
+                <div class="relative">
+                    <input 
+                        type="text" 
+                        id="searchInput"
+                        placeholder="@lang('Search by rule name...')" 
+                        class="pl-9 pr-3 py-2 w-64 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-gray-300"
+                    >
+                    <span class="absolute left-3 top-2.5 icon-search text-gray-400 text-sm"></span>
+                </div>
+                
+                <select 
+                    id="transactionTypeFilter"
+                    class="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-300"
+                >
+                    <option value="">@lang('All Types')</option>
+                    @foreach($transactionTypes as $id => $type)
+                        <option value="{{ $id }}">@lang($type['name'])</option>
+                    @endforeach
+                </select>
+                
+                <select 
+                    id="statusFilter"
+                    class="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-300"
+                >
+                    <option value="">@lang('All Status')</option>
+                    <option value="1">@lang('Active')</option>
+                    <option value="0">@lang('Inactive')</option>
+                </select>
+
+                <select 
+                    id="groupPointsFilter"
+                    class="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-300"
+                >
+                    <option value="">@lang('All Rules')</option>
+                    <option value="1">@lang('Different Points by Group')</option>
+                    <option value="0">@lang('Uniform Points')</option>
+                </select>
+            </div>
+        </div>
+    </div>
+
+    {{-- 表格列表 --}}
+    <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden">
+        <div class="overflow-x-auto">
+            <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
+                <thead class="bg-gray-50 dark:bg-gray-800">
+                    <tr>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('ID')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('Rule Name')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('Transaction Type')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('Group Settings')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('Points Configuration')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('Status')
+                        </th>
+                        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('Expiry')
+                        </th>
+                        <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                            @lang('Actions')
+                        </th>
+                    </tr>
+                </thead>
+                <tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
+                    @forelse($rules as $rule)
+                        @php
+                            $typeConfig = $transactionTypes[$rule->type_of_transaction] ?? [
+                                'name' => 'Unknown',
+                                'icon' => 'icon-question',
+                                'color' => 'gray'
+                            ];
+                            
+                            $colorClasses = [
+                                'blue' => 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300',
+                                'green' => 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300',
+                                'yellow' => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300',
+                                'purple' => 'bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300',
+                                'indigo' => 'bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300',
+                                'pink' => 'bg-pink-100 dark:bg-pink-900 text-pink-700 dark:text-pink-300',
+                                'orange' => 'bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300',
+                                'gray' => 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400',
+                            ];
+                        @endphp
+                        
+                        <tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
+                            {{-- ID --}}
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                                #{{ $rule->rule_id }}
+                            </td>
+
+                            {{-- 规则名称 --}}
+                            <td class="px-6 py-4">
+                                <div class="text-sm font-medium text-gray-900 dark:text-white">
+                                    {{ $rule->rule_name }}
+                                </div>
+                                @if($rule->comment)
+                                    <div class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-1">
+                                        {{ $rule->comment }}
+                                    </div>
+                                @endif
+                            </td>
+
+                            {{-- 交易类型 - 使用配置数组 --}}
+                            <td class="px-6 py-4 whitespace-nowrap">
+                                <span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium {{ $colorClasses[$typeConfig['color']] }}">
+                                    <span class="{{ $typeConfig['icon'] }} text-xs"></span>
+                                    @lang($typeConfig['name'])
+                                </span>
+                            </td>
+
+                            {{-- 群组设置类型 --}}
+                            <td class="px-6 py-4 whitespace-nowrap">
+                                @if($rule->enable_different_points_by_group)
+                                    <span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300">
+                                        <span class="icon-users text-xs"></span>
+                                        @lang('积分不同')
+                                    </span>
+                                @else
+                                    <span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300">
+                                        <span class="icon-star text-xs"></span>
+                                        @lang('积分相同')
+                                    </span>
+                                @endif
+                            </td>
+
+                            {{-- 积分配置 --}}
+                            <td class="px-6 py-4">
+                                @if($rule->enable_different_points_by_group)
+                                    @php
+                                        $groupPoints = json_decode($rule->customer_group_ids, true) ?: [];
+                                    @endphp
+                                    
+                                    @if(!empty($groupPoints))
+                                        <div class="space-y-1">
+                                            @foreach($groupPoints as $groupId => $points)
+                                                @if(!empty($groupId) && $groupId !== '')
+                                                    <div class="flex items-center justify-between gap-2 text-sm">
+                                                        <span class="text-gray-600 dark:text-gray-400">
+                                                            {{ $customerGroupsList[$groupId] ?? 'Group ' . $groupId }}:
+                                                        </span>
+                                                        <span class="font-medium text-yellow-600 dark:text-yellow-500">
+                                                            {{ $points }} @lang('pts')
+                                                        </span>
+                                                    </div>
+                                                @endif
+                                            @endforeach
+                                        </div>
+                                    @else
+                                        <span class="text-sm text-gray-400 dark:text-gray-500">
+                                            @lang('No groups configured')
+                                        </span>
+                                    @endif
+                                @else
+                                    <div class="flex items-center gap-1">
+                                        <span class="text-2xl font-bold text-yellow-600 dark:text-yellow-500">
+                                            {{ $rule->reward_point }}
+                                        </span>
+                                        <span class="text-xs text-gray-500">@lang('points')</span>
+                                        
+                                        @php
+                                            $customerGroups = $rule->customer_group_ids ? explode(',', $rule->customer_group_ids) : [];
+                                        @endphp
+                                        @if(!empty($customerGroups) && $customerGroups[0] !== '')
+                                            <div class="ml-2 flex flex-wrap gap-1">
+                                                @foreach(array_slice($customerGroups, 0, 2) as $groupId)
+                                                    <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
+                                                        {{ $customerGroupsList[$groupId] ?? 'Group ' . $groupId }}
+                                                    </span>
+                                                @endforeach
+                                                @if(count($customerGroups) > 2)
+                                                    <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400">
+                                                        +{{ count($customerGroups) - 2 }}
+                                                    </span>
+                                                @endif
+                                            </div>
+                                        @else
+                                            <span class="ml-2 text-xs text-gray-400">
+                                                (All Groups)
+                                            </span>
+                                        @endif
+                                    </div>
+                                @endif
+                            </td>
+
+                            {{-- 状态 --}}
+                            <td class="px-6 py-4 whitespace-nowrap">
+                                @if($rule->status)
+                                    <span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300">
+                                        <span class="icon-check-circle text-xs"></span>
+                                        @lang('Active')
+                                    </span>
+                                @else
+                                    <span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
+                                        <span class="icon-x-circle text-xs"></span>
+                                        @lang('Inactive')
+                                    </span>
+                                @endif
+                            </td>
+
+                            {{-- 过期信息 --}}
+                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
+                                @if($rule->expired_day > 0)
+                                    <span class="flex items-center gap-1">
+                                        <span class="icon-calendar text-xs"></span>
+                                        {{ $rule->expired_day }} @lang('days')
+                                    </span>
+                                @else
+                                    <span class="flex items-center gap-1">
+                                        <span class="icon-minus text-xs"></span>
+                                        @lang('Never')
+                                    </span>
+                                @endif
+                            </td>
+
+                            {{-- 操作按钮 --}}
+                            <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
+                                <div class="flex items-center justify-end gap-2">
+                                    <a 
+                                        href="{{ route('admin.reward-points.rules.edit', $rule->rule_id) }}" 
+                                        class="inline-flex items-center gap-1 px-3 py-1.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded-lg transition-colors"
+                                        title="@lang('Edit')"
+                                    >
+                                        <span class="icon-pencil text-sm"></span>
+                                        <span class="text-sm">@lang('Edit')</span>
+                                    </a>
+                                    
+                                    <form 
+                                        method="POST"
+                                        action="{{ route('admin.reward-points.rules.delete', $rule->rule_id) }}"
+                                        class="inline"
+                                        onsubmit="return confirm('@lang('Are you sure you want to delete this rule? This action cannot be undone.')');"
+                                    >
+                                        @csrf
+                                        @method('DELETE')
+                                        <button 
+                                            type="submit" 
+                                            class="inline-flex items-center gap-1 px-3 py-1.5 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 rounded-lg transition-colors"
+                                            title="@lang('Delete')"
+                                        >
+                                            <span class="icon-delete text-sm"></span>
+                                            <span class="text-sm">@lang('Delete')</span>
+                                        </button>
+                                    </form>
+                                </div>
+                            </td>
+                        </tr>
+                    @empty
+                        <tr>
+                            <td colspan="8" class="px-6 py-12 text-center">
+                                <div class="flex flex-col items-center gap-4">
+                                    <div class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
+                                        <span class="icon-star text-4xl text-gray-400"></span>
+                                    </div>
+                                    <div>
+                                        <p class="text-lg font-medium text-gray-600 dark:text-gray-400 mb-1">
+                                            @lang('No Reward Rules Found')
+                                        </p>
+                                        <p class="text-sm text-gray-500 dark:text-gray-500">
+                                            @lang('Get started by creating your first reward rule')
+                                        </p>
+                                    </div>
+                                    <a href="{{ route('admin.reward-points.rules.create') }}" class="primary-button mt-2">
+                                        @lang('Create Your First Rule')
+                                    </a>
+                                </div>
+                            </td>
+                        </tr>
+                    @endforelse
+                </tbody>
+            </table>
+        </div>
+
+        {{-- 分页 --}}
+        @if($rules->hasPages())
+            <div class="border-t border-gray-200 dark:border-gray-800 px-6 py-4">
+                <div class="flex flex-col sm:flex-row justify-between items-center gap-4">
+                    <div class="text-sm text-gray-600 dark:text-gray-400">
+                        {!! __('Showing :firstItem to :lastItem of :total items', [
+                            'firstItem' => $rules->firstItem(),
+                            'lastItem' => $rules->lastItem(),
+                            'total' => $rules->total()
+                        ]) !!}
+                    </div>
+
+                    <div class="flex gap-1">
+                        {{-- 上一页 --}}
+                        @if($rules->onFirstPage())
+                            <span class="px-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-400 cursor-not-allowed">
+                                <span class="icon-chevron-left text-sm"></span>
+                            </span>
+                        @else
+                            <a href="{{ $rules->previousPageUrl() }}" class="px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
+                                <span class="icon-chevron-left text-sm"></span>
+                            </a>
+                        @endif
+
+                        {{-- 页码 --}}
+                        @php
+                            $currentPage = $rules->currentPage();
+                            $lastPage = $rules->lastPage();
+                            $start = max(1, $currentPage - 2);
+                            $end = min($lastPage, $currentPage + 2);
+                        @endphp
+
+                        @if($start > 1)
+                            <a href="{{ $rules->url(1) }}" class="px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
+                                1
+                            </a>
+                            @if($start > 2)
+                                <span class="px-3 py-2 text-gray-500">...</span>
+                            @endif
+                        @endif
+
+                        @for($i = $start; $i <= $end; $i++)
+                            @if($i == $currentPage)
+                                <span class="px-3 py-2 rounded-lg bg-blue-600 text-white cursor-default">
+                                    {{ $i }}
+                                </span>
+                            @else
+                                <a href="{{ $rules->url($i) }}" class="px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
+                                    {{ $i }}
+                                </a>
+                            @endif
+                        @endfor
+
+                        @if($end < $lastPage)
+                            @if($end < $lastPage - 1)
+                                <span class="px-3 py-2 text-gray-500">...</span>
+                            @endif
+                            <a href="{{ $rules->url($lastPage) }}" class="px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
+                                {{ $lastPage }}
+                            </a>
+                        @endif
+
+                        {{-- 下一页 --}}
+                        @if($rules->hasMorePages())
+                            <a href="{{ $rules->nextPageUrl() }}" class="px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
+                                <span class="icon-chevron-right text-sm"></span>
+                            </a>
+                        @else
+                            <span class="px-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-400 cursor-not-allowed">
+                                <span class="icon-chevron-right text-sm"></span>
+                            </span>
+                        @endif
+                    </div>
+                </div>
+            </div>
+        @endif
+    </div>
+
+    <script>
+    // 实时搜索和筛选功能
+    document.addEventListener('DOMContentLoaded', function() {
+        const searchInput = document.getElementById('searchInput');
+        const transactionTypeFilter = document.getElementById('transactionTypeFilter');
+        const statusFilter = document.getElementById('statusFilter');
+        const groupPointsFilter = document.getElementById('groupPointsFilter');
+        
+        // 从服务器端获取交易类型映射(通过数据属性或全局变量)
+        const transactionTypeNames = @json($transactionTypes);
+        
+        function filterRules() {
+            const searchTerm = searchInput.value.toLowerCase();
+            const transactionTypeValue = transactionTypeFilter.value;
+            const statusValue = statusFilter.value;
+            const groupPointsValue = groupPointsFilter.value;
+            
+            const rows = document.querySelectorAll('tbody tr');
+            
+            rows.forEach(row => {
+                if (row.querySelector('td[colspan]')) return;
+                
+                const ruleName = row.querySelector('td:nth-child(2) .font-medium')?.innerText.toLowerCase() || '';
+                const transactionTypeSpan = row.querySelector('td:nth-child(3) .rounded-full');
+                const transactionTypeText = transactionTypeSpan?.innerText.trim() || '';
+                const statusBadge = row.querySelector('td:nth-child(6) .rounded-full')?.innerText.includes('Active') || false;
+                const groupPointsType = row.querySelector('td:nth-child(4) .rounded-full')?.innerText.includes('积分不同') || false;
+                
+                let show = true;
+                
+                // 搜索过滤
+                if (searchTerm && !ruleName.includes(searchTerm)) {
+                    show = false;
+                }
+                
+                // 交易类型过滤
+                if (transactionTypeValue !== '') {
+                    const expectedType = transactionTypeNames[transactionTypeValue]?.name || '';
+                    if (!transactionTypeText.includes(expectedType)) {
+                        show = false;
+                    }
+                }
+                
+                // 状态过滤
+                if (statusValue !== '') {
+                    const isActive = statusValue === '1';
+                    if (statusBadge !== isActive) {
+                        show = false;
+                    }
+                }
+                
+                // 群组积分类型过滤
+                if (groupPointsValue !== '') {
+                    const isDifferentPoints = groupPointsValue === '1';
+                    if (groupPointsType !== isDifferentPoints) {
+                        show = false;
+                    }
+                }
+                
+                row.style.display = show ? '' : 'none';
+            });
+        }
+        
+        if (searchInput) searchInput.addEventListener('keyup', filterRules);
+        if (transactionTypeFilter) transactionTypeFilter.addEventListener('change', filterRules);
+        if (statusFilter) statusFilter.addEventListener('change', filterRules);
+        if (groupPointsFilter) groupPointsFilter.addEventListener('change', filterRules);
+    });
+    </script>
+</x-admin::layouts>

+ 109 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/settings/index.blade.php

@@ -0,0 +1,109 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('积分系统配置')
+    </x-slot>
+
+    <div class="content">
+        <div class="flex justify-between items-center gap-2.5 mb-6">
+            <p class="text-xl text-gray-800 dark:text-white font-bold">
+                @lang('积分系统配置')
+            </p>
+        </div>
+
+        @if(session('success'))
+            <div class="alert alert-success mb-4">
+                {{ session('success') }}
+            </div>
+        @endif
+
+        @if(session('error'))
+            <div class="alert alert-danger mb-4">
+                {{ session('error') }}
+            </div>
+        @endif
+
+        {{-- 分组标签页 --}}
+        <div class="bg-white dark:bg-gray-900 rounded-lg shadow mb-6">
+            <div class="flex border-b border-gray-200 dark:border-gray-800 overflow-x-auto">
+                @foreach($groupData as $group => $data)
+                    <a href="{{ route('admin.reward-points.settings.index', ['group' => $group]) }}"
+                       class="px-6 py-3 text-sm font-medium whitespace-nowrap {{ $currentGroup === $group ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200' }}">
+                        @lang($data['name'])
+                    </a>
+                @endforeach
+            </div>
+        </div>
+
+        {{-- 配置表单 --}}
+        <div class="bg-white dark:bg-gray-900 rounded-lg shadow">
+            <form method="POST" action="{{ route('admin.reward-points.settings.save') }}">
+                @csrf
+                <input type="hidden" name="group" value="{{ $currentGroup }}">
+
+                <div class="p-6">
+                    @if($settings->isEmpty())
+                        <p class="text-gray-500">暂无配置项</p>
+                    @else
+                        @foreach($settings as $setting)
+                            <div class="mb-6">
+                                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
+                                    {{ $setting->name }}
+                                    @if($setting->description)
+                                        <span class="text-xs text-gray-500 block mt-1">{{ $setting->description }}</span>
+                                    @endif
+                                </label>
+
+                                @switch($setting->type)
+                                    @case('boolean')
+                                        <select name="settings[{{ $setting->code }}]" 
+                                                class="control w-full">
+                                            <option value="1" {{ $setting->value == '1' ? 'selected' : '' }}>启用</option>
+                                            <option value="0" {{ $setting->value == '0' ? 'selected' : '' }}>禁用</option>
+                                        </select>
+                                        @break
+
+                                    @case('number')
+                                        <input type="number" 
+                                               step="any"
+                                               name="settings[{{ $setting->code }}]" 
+                                               value="{{ old('settings.' . $setting->code, $setting->value) }}"
+                                               class="control w-full"
+                                               @if($setting->code === 'point_value') step="0.01" @endif
+                                               @if(in_array($setting->code, ['max_discount_percentage'])) min="0" max="100" @endif>
+                                        @break
+
+                                    @case('select')
+                                        <select name="settings[{{ $setting->code }}]" 
+                                                class="control w-full">
+                                            @foreach(($setting->options ?? []) as $key => $label)
+                                                <option value="{{ $key }}" {{ $setting->value == $key ? 'selected' : '' }}>
+                                                    {{ $label }}
+                                                </option>
+                                            @endforeach
+                                        </select>
+                                        @break
+
+                                    @default
+                                        <input type="text" 
+                                               name="settings[{{ $setting->code }}]" 
+                                               value="{{ old('settings.' . $setting->code, $setting->value) }}"
+                                               class="control w-full">
+                                @endswitch
+
+                                @error('settings.' . $setting->code)
+                                    <span class="text-red-500 text-xs mt-1">{{ $message }}</span>
+                                @enderror
+                            </div>
+                        @endforeach
+                    @endif
+                </div>
+
+                <div class="border-t border-gray-200 dark:border-gray-800 px-6 py-4 flex justify-end">
+                    <button type="submit" class="btn-primary">
+                        @lang('保存配置')
+                    </button>
+                </div>
+            </form>
+        </div>
+    </div>
+</x-admin::layouts>

+ 333 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/transactions/index.blade.php

@@ -0,0 +1,333 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('rewardpoints::rewardpoints.admin.transactions')
+    </x-slot:title>
+
+    {{-- 页面标题和操作按钮 --}}
+    <div class="flex flex-wrap gap-4 justify-between items-center mb-6">
+        <div class="flex items-center gap-3">
+            <div class="icon-list text-2xl text-blue-500"></div>
+            <p class="text-2xl text-gray-800 dark:text-white font-bold">
+                @if($viewType === 'earned')
+                    @lang('rewardpoints::rewardpoints.admin.earned-transactions')
+                @else
+                    @lang('rewardpoints::rewardpoints.admin.transactions')
+                @endif
+            </p>
+            <span class="px-2.5 py-0.5 text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full">
+                {{ $transactions->total() }}
+                @lang('rewardpoints::rewardpoints.admin.total')
+            </span>
+        </div>
+
+        <div class="flex gap-x-2.5 items-center">
+            {{-- 视图切换按钮 --}}
+            <div class="flex gap-2">
+                <a
+                    href="{{ route('admin.reward-points.transactions.index', array_merge(request()->except('view_type'), ['view_type' => 'all'])) }}"
+                    class="{{ $viewType !== 'earned' ? 'primary-button' : 'secondary-button' }}"
+                >
+                    <span class="icon-list text-lg"></span>
+                    @lang('rewardpoints::rewardpoints.admin.view-all-transactions')
+                </a>
+                <a
+                    href="{{ route('admin.reward-points.transactions.earned', array_merge(request()->except('view_type'))) }}"
+                    class="{{ $viewType === 'earned' ? 'primary-button' : 'secondary-button' }}"
+                >
+                    <span class="icon-arrow-up text-lg"></span>
+                    @lang('rewardpoints::rewardpoints.admin.view-earned-only')
+                </a>
+            </div>
+
+            <a
+                href="{{ route('admin.reward-points.transactions.export', request()->all()) }}"
+                class="secondary-button"
+            >
+                <span class="icon-download text-lg"></span>
+                @lang('Export')
+            </a>
+        </div>
+    </div>
+
+
+    {{-- 筛选器 --}}
+    <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 p-4 mb-6">
+        <form method="GET" action="{{ route('admin.reward-points.transactions.index') }}" class="flex flex-wrap items-end gap-4">
+            {{-- 开始日期 --}}
+            <div class="flex-1 min-w-[180px]">
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
+                    @lang('Start Date')
+                </label>
+                <input
+                    type="date"
+                    name="start_date"
+                    value="{{ $startDate }}"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+            </div>
+
+            {{-- 结束日期 --}}
+            <div class="flex-1 min-w-[180px]">
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
+                    @lang('End Date')
+                </label>
+                <input
+                    type="date"
+                    name="end_date"
+                    value="{{ $endDate }}"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+            </div>
+
+            {{-- 客户邮箱 --}}
+            <div class="flex-1 min-w-[200px]">
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
+                    @lang('Customer Email')
+                </label>
+                <input
+                    type="text"
+                    name="customer_email"
+                    value="{{ $customerEmail }}"
+                    placeholder="@lang('Search by email')"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+            </div>
+
+            {{-- 交易类型 --}}
+            <div class="flex-1 min-w-[160px]">
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
+                    @lang('Transaction Type')
+                </label>
+                <select
+                    name="transaction_type"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+                    <option value="">@lang('All Types')</option>
+                    <option value="1" {{ $transactionType == '1' ? 'selected' : '' }}>@lang('Order')</option>
+                    <option value="2" {{ $transactionType == '2' ? 'selected' : '' }}>@lang('Registration')</option>
+                    <option value="3" {{ $transactionType == '3' ? 'selected' : '' }}>@lang('Product Review')</option>
+                    <option value="4" {{ $transactionType == '4' ? 'selected' : '' }}>@lang('Daily Sign In')</option>
+                    <option value="5" {{ $transactionType == '5' ? 'selected' : '' }}>@lang('Referral')</option>
+                    <option value="6" {{ $transactionType == '6' ? 'selected' : '' }}>@lang('Birthday')</option>
+                    <option value="8" {{ $transactionType == '8' ? 'selected' : '' }}>@lang('Subscribe')</option>
+                    <option value="0" {{ $transactionType == '0' ? 'selected' : '' }}>@lang('Admin Adjustment')</option>
+                    <option value="99" {{ $transactionType == '99' ? 'selected' : '' }}>@lang('Admin Action')</option>
+                </select>
+            </div>
+
+            {{-- 金额类型 --}}
+            <div class="flex-1 min-w-[140px]">
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
+                    @lang('Amount Type')
+                </label>
+                <select
+                    name="amount_type"
+                    class="flex w-full min-h-[39px] py-2 px-3 border border-gray-300 dark:border-gray-800 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-all hover:border-gray-400 dark:hover:border-gray-400 focus:border-gray-400 dark:focus:border-gray-400"
+                >
+                    <option value="">@lang('All')</option>
+                    <option value="earned" {{ $amountType == 'earned' ? 'selected' : '' }}>@lang('Earned')</option>
+                    <option value="redeemed" {{ $amountType == 'redeemed' ? 'selected' : '' }}>@lang('Redeemed')</option>
+                </select>
+            </div>
+
+            {{-- 操作按钮 --}}
+            <div class="flex gap-2">
+                <button
+                    type="submit"
+                    class="primary-button"
+                >
+                    <span class="icon-search text-lg"></span>
+                    @lang('Search')
+                </button>
+
+                <a
+                    href="{{ route('admin.reward-points.transactions.index') }}"
+                    class="secondary-button"
+                >
+                    <span class="icon-reset text-lg"></span>
+                    @lang('Reset')
+                </a>
+            </div>
+        </form>
+    </div>
+    {{-- 统计卡片 - 强制一行三列 --}}
+    <div class="grid grid-cols-3 gap-4 mb-6">
+        {{-- 总赚取积分 --}}
+        <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden">
+            <div class="p-5">
+                <div class="flex items-center justify-between">
+                    <div>
+                        <p class="text-sm text-gray-500 dark:text-gray-400">
+                            @lang('Total Points Earned')
+                        </p>
+                        <p class="text-3xl font-bold text-green-600 dark:text-green-400 mt-2">
+                            {{ number_format($totalEarned) }}
+                        </p>
+                    </div>
+                    <div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
+                        <span class="icon-arrow-up text-2xl text-green-600 dark:text-green-400"></span>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        {{-- 总兑换积分 --}}
+        <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden">
+            <div class="p-5">
+                <div class="flex items-center justify-between">
+                    <div>
+                        <p class="text-sm text-gray-500 dark:text-gray-400">
+                            @lang('Total Points Redeemed')
+                        </p>
+                        <p class="text-3xl font-bold text-red-600 dark:text-red-400 mt-2">
+                            {{ number_format($totalRedeemed) }}
+                        </p>
+                    </div>
+                    <div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
+                        <span class="icon-arrow-down text-2xl text-red-600 dark:text-red-400"></span>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        {{-- 总交易数 --}}
+        <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden">
+            <div class="p-5">
+                <div class="flex items-center justify-between">
+                    <div>
+                        <p class="text-sm text-gray-500 dark:text-gray-400">
+                            @lang('Total Transactions')
+                        </p>
+                        <p class="text-3xl font-bold text-blue-600 dark:text-blue-400 mt-2">
+                            {{ number_format($totalTransactions) }}
+                        </p>
+                    </div>
+                    <div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
+                        <span class="icon-list text-2xl text-blue-600 dark:text-blue-400"></span>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    {{-- 表格 --}}
+    <div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden">
+        <div class="overflow-x-auto">
+            <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
+                <thead class="bg-gray-50 dark:bg-gray-800">
+                <tr>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                        @lang('ID')
+                    </th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                        @lang('Customer')
+                    </th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                        @lang('Type')
+                    </th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                        @lang('Amount')
+                    </th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                        @lang('Balance')
+                    </th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                        @lang('Date')
+                    </th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                        @lang('Description')
+                    </th>
+                </tr>
+                </thead>
+                <tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
+                @forelse($transactions as $transaction)
+                    <tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
+                        <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                            #{{ $transaction->history_id }}
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap">
+                            <div class="text-sm text-gray-900 dark:text-gray-300">
+                                @if($transaction->customer)
+                                    {{ $transaction->customer->first_name }} {{ $transaction->customer->last_name }}
+                                @else
+                                    N/A
+                                @endif
+                            </div>
+                            <div class="text-sm text-gray-500 dark:text-gray-400">
+                                {{ $transaction->customer ? $transaction->customer->email : 'N/A' }}
+                            </div>
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap">
+                            @php
+                                $typeNames = [
+                                    1 => 'rewardpoints::rewardpoints.admin.transaction-types.order',
+                                    2 => 'rewardpoints::rewardpoints.admin.transaction-types.registration',
+                                    3 => 'rewardpoints::rewardpoints.admin.transaction-types.review',
+                                    4 => 'rewardpoints::rewardpoints.admin.transaction-types.sign-in',
+                                    5 => 'rewardpoints::rewardpoints.admin.transaction-types.referral',
+                                    6 => 'rewardpoints::rewardpoints.admin.transaction-types.birthday',
+                                    7 => 'rewardpoints::rewardpoints.admin.transaction-types.share',
+                                    8 => 'rewardpoints::rewardpoints.admin.transaction-types.subscribe',
+                                    0 => 'rewardpoints::rewardpoints.admin.admin-adjustment',
+                                    99 => 'rewardpoints::rewardpoints.admin.admin-action',
+                                ];
+                            @endphp
+                            <span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full
+            @if($transaction->amount > 0)
+                bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200
+            @else
+                bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200
+            @endif">
+        @lang($typeNames[$transaction->type_of_transaction] ?? 'Unknown')
+    </span>
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap text-sm">
+                                <span class="font-semibold
+                                    @if($transaction->amount > 0)
+                                        text-green-600 dark:text-green-400
+                                    @else
+                                        text-red-600 dark:text-red-400
+                                    @endif">
+                                    {{ $transaction->amount > 0 ? '+' : '' }}{{ number_format($transaction->amount) }}
+                                </span>
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                            {{ number_format($transaction->balance) }}
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-300">
+                            {{ \Carbon\Carbon::parse($transaction->transaction_time)->format('Y-m-d H:i') }}
+                        </td>
+                        <td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-300 max-w-xs truncate">
+                            {{ $transaction->transaction_detail }}
+                        </td>
+                    </tr>
+                @empty
+                    <tr>
+                        <td colspan="7" class="px-6 py-12 text-center">
+                            <div class="flex flex-col items-center gap-4">
+                                <div class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
+                                    <span class="icon-list text-4xl text-gray-400"></span>
+                                </div>
+                                <div>
+                                    <p class="text-lg font-medium text-gray-600 dark:text-gray-400 mb-1">
+                                        @lang('No transactions found')
+                                    </p>
+                                    <p class="text-sm text-gray-500 dark:text-gray-500">
+                                        @lang('Try adjusting your filters')
+                                    </p>
+                                </div>
+                            </div>
+                        </td>
+                    </tr>
+                @endforelse
+                </tbody>
+            </table>
+        </div>
+
+        {{-- 分页 --}}
+        @if($transactions->hasPages())
+            <div class="p-4 border-t border-gray-200 dark:border-gray-800">
+                {{ $transactions->links() }}
+            </div>
+        @endif
+    </div>
+</x-admin::layouts>

+ 173 - 0
packages/Longyi/RewardPoints/src/Resources/views/shop/customer/index.blade.php

@@ -0,0 +1,173 @@
+@extends('shop::layouts.master')
+
+@section('page_title')
+    My Reward Points
+@stop
+
+@section('content-wrapper')
+    <div class="account-grid">
+        <div class="account-for">
+            <h1>My Reward Points</h1>
+        </div>
+
+        <div class="account-table-content">
+            <div class="row">
+                <div class="col-12 col-md-4">
+                    <div class="card points-summary">
+                        <div class="card-body text-center">
+                            <h3 class="points-balance">{{ $points }}</h3>
+                            <p class="text-muted">Available Points</p>
+                        </div>
+                    </div>
+                </div>
+                
+                <div class="col-12 col-md-8">
+                    <div class="card">
+                        <div class="card-header">
+                            <h4>Daily Sign In</h4>
+                        </div>
+                        <div class="card-body text-center">
+                            <button 
+                                id="signInBtn" 
+                                class="btn btn-primary btn-lg"
+                                onclick="handleSignIn()"
+                            >
+                                Sign In Now
+                            </button>
+                            <p class="mt-3" id="streakInfo"></p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="card mt-4">
+                <div class="card-header">
+                    <h4>Points History</h4>
+                </div>
+                <div class="card-body table-responsive">
+                    <table class="table">
+                        <thead>
+                            <tr>
+                                <th>Date</th>
+                                <th>Type</th>
+                                <th>Description</th>
+                                <th>Points</th>
+                                <th>Balance</th>
+                                <th>Status</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            @forelse($history as $item)
+                                <tr>
+                                    <td>{{ $item->transaction_time->format('Y-m-d H:i') }}</td>
+                                    <td>
+                                        @switch($item->type_of_transaction)
+                                            @case(1)
+                                                <span class="badge badge-info">Order</span>
+                                                @break
+                                            @case(2)
+                                                <span class="badge badge-success">Registration</span>
+                                                @break
+                                            @case(3)
+                                                <span class="badge badge-warning">Review</span>
+                                                @break
+                                            @case(4)
+                                                <span class="badge badge-primary">Sign In</span>
+                                                @break
+                                            @case(5)
+                                                <span class="badge badge-secondary">Referral</span>
+                                                @break
+                                            @case(6)
+                                                <span class="badge badge-danger">Birthday</span>
+                                                @break
+                                            @default
+                                                <span class="badge badge-dark">Other</span>
+                                        @endswitch
+                                    </td>
+                                    <td>{{ $item->transaction_detail ?? '-' }}</td>
+                                    <td class="{{ $item->amount > 0 ? 'text-success' : 'text-danger' }}">
+                                        {{ $item->amount > 0 ? '+' : '' }}{{ $item->amount }}
+                                    </td>
+                                    <td>{{ $item->balance }}</td>
+                                    <td>
+                                        @switch($item->status)
+                                            @case(0)
+                                                <span class="badge badge-warning">Pending</span>
+                                                @break
+                                            @case(1)
+                                                <span class="badge badge-success">Completed</span>
+                                                @break
+                                            @case(2)
+                                                <span class="badge badge-danger">Cancelled</span>
+                                                @break
+                                            @case(3)
+                                                <span class="badge badge-dark">Expired</span>
+                                                @break
+                                        @endswitch
+                                    </td>
+                                </tr>
+                            @empty
+                                <tr>
+                                    <td colspan="6" class="text-center">No points history yet</td>
+                                </tr>
+                            @endforelse
+                        </tbody>
+                    </table>
+                    
+                    @if($history->hasPages())
+                        <div class="pagination-wrapper mt-3">
+                            {{ $history->links() }}
+                        </div>
+                    @endif
+                </div>
+            </div>
+        </div>
+    </div>
+@stop
+
+@push('scripts')
+    <script>
+        function handleSignIn() {
+            const signInBtn = document.getElementById('signInBtn');
+            signInBtn.disabled = true;
+            
+            fetch('{{ route('customer.reward-points.sign-in') }}', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                    'X-CSRF-TOKEN': '{{ csrf_token() }}'
+                }
+            })
+            .then(response => response.json())
+            .then(data => {
+                if (data.success) {
+                    alert('Sign in successful! You earned ' + data.points + ' points!');
+                    location.reload();
+                } else {
+                    alert(data.error || 'Failed to sign in');
+                    signInBtn.disabled = false;
+                }
+            })
+            .catch(error => {
+                console.error('Error:', error);
+                alert('An error occurred during sign in');
+                signInBtn.disabled = false;
+            });
+        }
+        
+        fetch('{{ route('customer.reward-points.sign-status') }}')
+            .then(response => response.json())
+            .then(data => {
+                if (data.signed_today) {
+                    document.getElementById('signInBtn').disabled = true;
+                    document.getElementById('signInBtn').textContent = 'Signed In Today';
+                }
+                
+                if (data.current_streak > 0) {
+                    document.getElementById('streakInfo').innerHTML = 
+                        'Current Streak: <strong>' + data.current_streak + '</strong> days';
+                }
+            })
+            .catch(error => console.error('Error checking sign status:', error));
+    </script>
+@endpush

+ 169 - 0
packages/Longyi/RewardPoints/src/Routes/routes.php

@@ -0,0 +1,169 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+
+// 前台路由 - 客户中心
+Route::group(['middleware' => ['web', 'customer'], 'prefix' => 'customer'], function () {
+    Route::get('reward-points', [
+        'as' => 'customer.reward-points.index',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@index',
+        'view' => 'rewardpoints::shop.customer.index'
+    ]);
+
+    Route::post('reward-points/sign-in', [
+        'as' => 'customer.reward-points.sign-in',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@signIn'
+    ]);
+
+    Route::get('reward-points/sign-status', [
+        'as' => 'customer.reward-points.sign-status',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@getSignStatus'
+    ]);
+
+    Route::post('apply-reward-points', [
+        'as' => 'checkout.apply-reward-points',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@applyPoints'
+    ]);
+
+    Route::post('remove-reward-points', [
+        'as' => 'checkout.remove-reward-points',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@removePoints'
+    ]);
+
+    Route::get('reward-points-info', [
+        'as' => 'checkout.reward-points-info',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@getPointsInfo'
+    ]);
+});
+
+// 后台路由
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin/reward-points'], function () {
+
+    // ========== 规则管理 - 静态路由 ==========
+    Route::get('rules', [
+        'as' => 'admin.reward-points.rules.index',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\RuleController@index'
+    ]);
+
+    Route::get('rules/create', [
+        'as' => 'admin.reward-points.rules.create',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\RuleController@create'
+    ]);
+
+    Route::post('rules', [
+        'as' => 'admin.reward-points.rules.store',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\RuleController@store'
+    ]);
+
+    // ========== 客户管理 - 静态路由(重要:放在动态路由之前) ==========
+    // 导出路由
+    Route::get('customers/export', [
+        'as' => 'admin.reward-points.customers.export',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@export'
+    ]);
+
+    // 调整积分表单路由
+    Route::get('customers/adjust-points', [
+        'as' => 'admin.reward-points.customers.adjust-form',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@adjustPointsForm'
+    ]);
+
+    // 提交调整积分
+    Route::post('customers/adjust-points', [
+        'as' => 'admin.reward-points.customers.adjust-points.submit',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@adjustPointsByIdOrEmail'
+    ]);
+
+    // 批量更新
+    Route::post('customers/bulk-update', [
+        'as' => 'admin.reward-points.customers.bulk-update',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@bulkUpdate'
+    ]);
+
+    // 添加积分
+    Route::post('customers/add-points', [
+        'as' => 'admin.reward-points.customers.add-points',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@addPoints'
+    ]);
+
+    // 扣除积分
+    Route::post('customers/deduct-points', [
+        'as' => 'admin.reward-points.customers.deduct-points',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@deductPoints'
+    ]);
+
+    // ========== 报表管理 - 静态路由 ==========
+    Route::get('reports/export', [
+        'as' => 'admin.reward-points.reports.export',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\ReportController@export'
+    ]);
+
+    // ========== 设置管理 - 静态路由 ==========
+    Route::get('settings', [
+        'as' => 'admin.reward-points.settings.index',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\SettingController@index'
+    ]);
+
+    Route::post('settings', [
+        'as' => 'admin.reward-points.settings.save',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\SettingController@save'
+    ]);
+
+    // ========== 规则管理 - 动态路由 ==========
+    Route::get('rules/{id}/edit', [
+        'as' => 'admin.reward-points.rules.edit',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\RuleController@edit'
+    ]);
+
+    Route::put('rules/{id}', [
+        'as' => 'admin.reward-points.rules.update',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\RuleController@update'
+    ]);
+
+    Route::delete('rules/{id}', [
+        'as' => 'admin.reward-points.rules.delete',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\RuleController@destroy'
+    ]);
+
+    Route::post('rules/{id}/update-status', [
+        'as' => 'admin.reward-points.rules.update-status',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\RuleController@updateStatus'
+    ]);
+
+    // ========== 客户管理 - 动态路由 ==========
+    // 客户列表(放在动态路由前面也可以,但为了清晰,放在这里)
+    Route::get('customers', [
+        'as' => 'admin.reward-points.customers.index',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@index'
+    ]);
+
+    // 客户详情(动态参数路由,必须放在所有静态路由之后)
+    Route::get('customers/{customerId}', [
+        'as' => 'admin.reward-points.customers.show',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\CustomerController@show'
+    ]);
+
+    // ========== 报表管理 - 静态路由 ==========
+    Route::get('reports/export', [
+        'as' => 'admin.reward-points.reports.export',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\ReportController@export'
+    ]);
+// ========== 报表管理 - 动态路由 ==========
+    Route::get('reports', [
+        'as' => 'admin.reward-points.reports.index',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\ReportController@index'
+    ]);
+// 积分获取记录专用路由
+    Route::get('transactions/earned', [
+        'as' => 'admin.reward-points.transactions.earned',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\TransactionController@earnedIndex'
+    ]);
+    Route::get('transactions', [
+        'as' => 'admin.reward-points.transactions.index',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\TransactionController@index'
+    ]);
+    Route::get('transactions/export', [
+        'as' => 'admin.reward-points.transactions.export',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\TransactionController@export'
+    ]);
+});

+ 263 - 0
packages/Longyi/RewardPoints/src/Services/CartRewardPoints.php

@@ -0,0 +1,263 @@
+<?php
+
+namespace Longyi\RewardPoints\Services;
+
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
+use Longyi\RewardPoints\Models\RewardPointCustomer;
+use Webkul\Checkout\Facades\Cart;
+use Webkul\Customer\Facades\Customer;
+
+class CartRewardPoints
+{
+    /**
+     * Session key for points used in cart
+     */
+    const POINTS_USED_KEY = 'reward_points_used';
+    
+    /**
+     * @var RewardPointRepository
+     */
+    protected $rewardPointRepository;
+    
+    /**
+     * CartRewardPoints constructor.
+     */
+    public function __construct()
+    {
+        $this->rewardPointRepository = app(RewardPointRepository::class);
+    }
+    
+    /**
+     * Get available points for current customer
+     */
+    public function getAvailablePoints()
+    {
+        if (!Customer::check()) {
+            return 0;
+        }
+        
+        return $this->rewardPointRepository->getCustomerPoints(Customer::getCustomerId());
+    }
+    
+    /**
+     * Get points used in current cart
+     */
+    public function getPointsUsed()
+    {
+        if (!session()->has(self::POINTS_USED_KEY)) {
+            return 0;
+        }
+        
+        return session()->get(self::POINTS_USED_KEY);
+    }
+    
+    /**
+     * Get discount amount from points
+     */
+    public function getDiscountAmount($points = null)
+    {
+        $pointsUsed = $points ?: $this->getPointsUsed();
+        
+        if ($pointsUsed <= 0) {
+            return 0;
+        }
+        
+        $pointValue = config('rewardpoints.general.point_value', 0.01);
+        
+        return $pointsUsed * $pointValue;
+    }
+    
+    /**
+     * Apply points to cart
+     */
+    public function applyPoints($points)
+    {
+        $cart = Cart::getCart();
+        
+        if (!$cart) {
+            return false;
+        }
+        
+        $availablePoints = $this->getAvailablePoints();
+        
+        if ($points <= 0) {
+            $this->removePoints();
+            return false;
+        }
+        
+        if ($points > $availablePoints) {
+            return false;
+        }
+        
+        // Calculate maximum points that can be used based on cart total
+        $maxPointsByAmount = $this->getMaxPointsByCartTotal();
+        
+        if ($points > $maxPointsByAmount) {
+            return false;
+        }
+        
+        // Save points to session
+        session()->put(self::POINTS_USED_KEY, $points);
+        
+        // Recalculate cart
+        Cart::collectTotals();
+        
+        return true;
+    }
+    
+    /**
+     * Remove points from cart
+     */
+    public function removePoints()
+    {
+        session()->forget(self::POINTS_USED_KEY);
+        Cart::collectTotals();
+        
+        return true;
+    }
+    
+    /**
+     * Calculate discount based on points
+     */
+    public function calculateDiscount()
+    {
+        $pointsUsed = $this->getPointsUsed();
+        
+        if ($pointsUsed <= 0) {
+            return 0;
+        }
+        
+        $discountAmount = $this->getDiscountAmount($pointsUsed);
+        
+        return $discountAmount;
+    }
+    
+    /**
+     * Get maximum points that can be used based on cart total
+     */
+    protected function getMaxPointsByCartTotal()
+    {
+        $cart = Cart::getCart();
+        
+        if (!$cart) {
+            return 0;
+        }
+        
+        $cartTotal = $cart->grand_total;
+        
+        // Get maximum discount percentage (default: 100%)
+        $maxDiscountPercentage = core()->getConfigData('rewardpoints.general.max_discount_percentage', 100);
+        
+        // Calculate maximum discount amount
+        $maxDiscountAmount = ($cartTotal * $maxDiscountPercentage) / 100;
+        
+        // Convert discount amount to points
+        $pointValue = core()->getConfigData('rewardpoints.general.point_value', 0.01);
+        $maxPoints = $pointValue > 0 ? floor($maxDiscountAmount / $pointValue) : 0;
+        
+        return $maxPoints;
+    }
+    
+    /**
+     * Get discount details for cart
+     */
+    public function getDiscountDetails()
+    {
+        $pointsUsed = $this->getPointsUsed();
+        
+        if ($pointsUsed <= 0) {
+            return null;
+        }
+        
+        $discountAmount = $this->getDiscountAmount();
+        
+        return [
+            'points_used' => $pointsUsed,
+            'discount_amount' => $discountAmount,
+            'formatted_discount' => core()->formatPrice($discountAmount),
+            'available_points' => $this->getAvailablePoints(),
+            'points_remaining' => $this->getAvailablePoints() - $pointsUsed
+        ];
+    }
+    
+    /**
+     * Process order points
+     */
+    public function processOrderPoints($order)
+    {
+        $pointsUsed = $this->getPointsUsed();
+        
+        if ($pointsUsed <= 0) {
+            return false;
+        }
+        
+        $customerId = $order->customer_id;
+        
+        if (!$customerId) {
+            return false;
+        }
+        
+        // Deduct points from customer
+        $discountAmount = $this->getDiscountAmount($pointsUsed);
+        
+        $result = $this->rewardPointRepository->deductPoints(
+            $customerId,
+            $pointsUsed,
+            $order->id,
+            "Points redeemed for order #{$order->increment_id} (Discount: " . core()->formatPrice($discountAmount) . ")"
+        );
+        
+        // Clear session
+        $this->removePoints();
+        
+        return $result;
+    }
+    
+    /**
+     * Get points value in currency
+     */
+    public function getPointsValue($points)
+    {
+        $pointValue = core()->getConfigData('rewardpoints.general.point_value', 0.01);
+        return $points * $pointValue;
+    }
+    
+    /**
+     * Get currency value in points
+     */
+    public function getPointsFromValue($value)
+    {
+        $pointValue = core()->getConfigData('rewardpoints.general.point_value', 0.01);
+        return $pointValue > 0 ? floor($value / $pointValue) : 0;
+    }
+    
+    /**
+     * Validate if points can be applied to cart
+     */
+    public function validatePoints($points)
+    {
+        $cart = Cart::getCart();
+        
+        if (!$cart) {
+            return 'Cart not found';
+        }
+        
+        $availablePoints = $this->getAvailablePoints();
+        
+        if ($points <= 0) {
+            return 'Points must be greater than 0';
+        }
+        
+        if ($points > $availablePoints) {
+            return "You only have {$availablePoints} points available";
+        }
+        
+        $maxPoints = $this->getMaxPointsByCartTotal();
+        
+        if ($points > $maxPoints) {
+            return "Maximum {$maxPoints} points can be used for this order";
+        }
+        
+        return true;
+    }
+}

+ 7 - 0
packages/Webkul/Admin/src/Http/Controllers/Customers/CustomerController.php

@@ -110,11 +110,18 @@ class CustomerController extends Controller
         $customer = $this->customerRepository->create($data);
 
         if (core()->getConfigData('emails.general.notifications.emails.general.notifications.customer_account_credentials')) {
+            // 临时禁用邮件发送,用于测试
+            Log::info('Customer registered but email sending disabled for testing', [
+                'customer_id' => $customer->id,
+                'email' => $customer->email
+            ]);
+            /*
             try {
                 Mail::queue(new NewCustomerNotification($customer, $password));
             } catch (\Exception $e) {
                 report($e);
             }
+            */
         }
 
         Event::dispatch('customer.create.after', $customer);

+ 6 - 2
packages/Webkul/Admin/src/Listeners/Customer.php

@@ -19,8 +19,12 @@ class Customer extends Base
             if (! core()->getConfigData('emails.general.notifications.emails.general.notifications.customer_registration_confirmation_mail_to_admin')) {
                 return;
             }
-
-            Mail::queue(new RegistrationNotification($customer));
+             // 临时禁用邮件发送,用于测试
+            Log::info('Customer registered but email sending disabled for testing', [
+                'customer_id' => $customer->id,
+                'email' => $customer->email
+            ]);
+            //Mail::queue(new RegistrationNotification($customer));
         } catch (\Exception $e) {
             report($e);
         }