Przeglądaj źródła

会员成长值

bianjunhui 2 tygodni temu
rodzic
commit
75fc2a748e

+ 30 - 0
packages/Longyi/RewardPoints/src/Console/Commands/CheckExpiredGrowthValues.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Longyi\RewardPoints\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\RewardPoints\Services\GrowthValueService;
+
+class CheckExpiredGrowthValues extends Command
+{
+    protected $signature = 'growth-value:check-expired';
+
+    protected $description = '检查并处理过期的成长值';
+
+    protected $growthValueService;
+
+    public function __construct(GrowthValueService $growthValueService)
+    {
+        parent::__construct();
+        $this->growthValueService = $growthValueService;
+    }
+
+    public function handle()
+    {
+        $this->info('开始检查过期的成长值...');
+
+        $this->growthValueService->checkExpiredGrowthValues();
+
+        $this->info('过期成长值检查完成!');
+    }
+}

+ 62 - 0
packages/Longyi/RewardPoints/src/Console/Commands/InitializeGrowthSettings.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace Longyi\RewardPoints\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\RewardPoints\Repositories\RewardPointSettingRepository;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class InitializeGrowthSettings extends Command
+{
+    protected $signature = 'reward-points:init-growth-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->addItems();
+
+            return 0;
+
+        } catch (\Exception $e) {
+            $this->error('Error initializing settings: ' . $e->getMessage());
+            return 1;
+        }
+    }
+
+    /**
+     * 添加积分模块的菜单项到动态菜单
+     */
+    protected function addItems()
+    {
+        $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;
+        }
+
+
+        $this->info('✓ Reward points menu items added to dynamic menu successfully!');
+    }
+}

+ 83 - 0
packages/Longyi/RewardPoints/src/Database/Migrations/2026_01_01_000002_create_points_tables.php

@@ -0,0 +1,83 @@
+<?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_growth_value_customer')) {
+            Schema::create('mw_growth_value_customer', function (Blueprint $table) {
+                $table->increments('id');
+                $table->unsignedInteger('customer_id');
+                $table->unsignedBigInteger('growth_value')->default(0)->comment('当前成长值');
+                $table->string('growth_level', 20)->default('V0')->comment('成长值等级:V0, V1, V2, V3...');
+                $table->timestamp('first_order_completed_at')->nullable()->comment('首单完成时间');
+                $table->timestamp('level_updated_at')->nullable()->comment('等级更新时间');
+                $table->timestamp('growth_value_expires_at')->nullable()->comment('成长值过期时间');
+                $table->timestamps();
+
+                $table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade');
+                $table->unique('customer_id', 'uk_customer_id');
+                $table->index('growth_level', 'idx_growth_level');
+            });
+        }
+
+        if (!Schema::hasTable('mw_growth_value_history')) {
+            Schema::create('mw_growth_value_history', function (Blueprint $table) {
+                $table->increments('history_id');
+                $table->unsignedInteger('customer_id');
+                $table->unsignedInteger('order_id')->nullable()->comment('关联订单ID');
+                $table->decimal('order_amount', 12, 4)->default(0)->comment('订单金额');
+                $table->integer('growth_value_earned')->default(0)->comment('获得的成长值');
+                $table->integer('total_growth_value')->default(0)->comment('累计成长值');
+                $table->string('level_before', 20)->nullable()->comment('升级前等级');
+                $table->string('level_after', 20)->nullable()->comment('升级后等级');
+                $table->string('event_type', 50)->default('order')->comment('事件类型:order, admin_adjust');
+                $table->text('description')->nullable()->comment('描述');
+                $table->timestamp('expires_at')->nullable()->comment('过期时间');
+                $table->timestamp('created_at')->nullable();
+
+                $table->index('customer_id', 'idx_customer_id');
+                $table->index('order_id', 'idx_order_id');
+                $table->index('created_at', 'idx_created_at');
+
+                $table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade');
+            });
+        }
+
+        if (!Schema::hasTable('mw_growth_value_levels')) {
+            Schema::create('mw_growth_value_levels', function (Blueprint $table) {
+                $table->increments('id');
+                $table->string('level_code', 20)->unique()->comment('等级代码:V0, V1, V2, V3...');
+                $table->string('level_name', 100)->comment('等级名称:普通会员、铜牌会员、银牌会员...');
+                $table->integer('min_growth_value')->default(0)->comment('最低成长值要求');
+                $table->integer('max_growth_value')->nullable()->comment('最高成长值要求,NULL表示无上限');
+                $table->boolean('require_first_order')->default(false)->comment('是否需要完成首单');
+                $table->integer('discount_rate')->default(0)->comment('额外折扣率(百分比),在会员组折扣基础上叠加');
+                $table->text('benefits')->nullable()->comment('权益说明(JSON格式)');
+                $table->string('icon', 255)->nullable()->comment('等级图标URL');
+                $table->integer('sort_order')->default(0)->comment('排序');
+                $table->boolean('is_active')->default(true)->comment('是否启用');
+                $table->timestamps();
+
+                $table->index('sort_order', 'idx_sort_order');
+                $table->index('min_growth_value', 'idx_min_growth_value');
+            });
+        }
+    }
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('mw_growth_value_levels');
+        Schema::dropIfExists('mw_growth_value_history');
+        Schema::dropIfExists('mw_growth_value_customer');
+    }
+};

+ 97 - 0
packages/Longyi/RewardPoints/src/Database/Seeders/GrowthValueSeeder.php

@@ -0,0 +1,97 @@
+<?php
+namespace Longyi\RewardPoints\Database\Seeders;
+use Illuminate\Database\Seeder;
+use Longyi\RewardPoints\Models\GrowthValueLevel;
+use Longyi\RewardPoints\Models\RewardPointSetting;
+
+class GrowthValueSeeder extends Seeder
+{
+    public function run()
+    {
+        $levels = [
+            [
+                'level_code' => 'V0',
+                'level_name' => '普通会员',
+                'min_growth_value' => 0,
+                'max_growth_value' => null,
+                'require_first_order' => false,
+                'discount_rate' => 0,
+                'benefits' => ['基础会员服务'],
+                'icon' => null,
+                'sort_order' => 0,
+                'is_active' => true,
+            ],
+            [
+                'level_code' => 'V1',
+                'level_name' => '铜牌会员',
+                'min_growth_value' => 0,
+                'max_growth_value' => 999,
+                'require_first_order' => true,
+                'discount_rate' => 2,
+                'benefits' => ['完成首单升级', '享受2%额外折扣', '优先客服支持'],
+                'icon' => null,
+                'sort_order' => 1,
+                'is_active' => true,
+            ],
+            [
+                'level_code' => 'V2',
+                'level_name' => '银牌会员',
+                'min_growth_value' => 1000,
+                'max_growth_value' => 4999,
+                'require_first_order' => false,
+                'discount_rate' => 5,
+                'benefits' => ['享受5%额外折扣', '专属优惠券', '生日礼品', '优先发货'],
+                'icon' => null,
+                'sort_order' => 2,
+                'is_active' => true,
+            ],
+            [
+                'level_code' => 'V3',
+                'level_name' => '金牌会员',
+                'min_growth_value' => 5000,
+                'max_growth_value' => null,
+                'require_first_order' => false,
+                'discount_rate' => 10,
+                'benefits' => ['享受10%额外折扣', '专属客服经理', '新品优先体验', '免费配送', '专属活动邀请'],
+                'icon' => null,
+                'sort_order' => 3,
+                'is_active' => true,
+            ],
+        ];
+
+        foreach ($levels as $levelData) {
+            GrowthValueLevel::updateOrCreate(
+                ['level_code' => $levelData['level_code']],
+                $levelData
+            );
+        }
+
+        $settings = [
+            [
+                'code' => 'growth_value_ratio',
+                'name' => '成长值计算比例',
+                'value' => '1',
+                'type' => 'number',
+                'group' => 'growth_value',
+                'description' => '每消费1元获得的成长值数量',
+                'is_enabled' => true,
+            ],
+            [
+                'code' => 'growth_value_validity_days',
+                'name' => '成长值有效期(天)',
+                'value' => '365',
+                'type' => 'number',
+                'group' => 'growth_value',
+                'description' => '成长值的有效期,0表示永久有效',
+                'is_enabled' => true,
+            ],
+        ];
+
+        foreach ($settings as $settingData) {
+            RewardPointSetting::updateOrCreate(
+                ['code' => $settingData['code']],
+                $settingData
+            );
+        }
+    }
+}

+ 135 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/Admin/GrowthValueController.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace Longyi\RewardPoints\Http\Controllers\Admin;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\Validator;
+use Longyi\RewardPoints\Services\GrowthValueService;
+use Longyi\RewardPoints\Models\GrowthValueCustomer;
+use Longyi\RewardPoints\Models\GrowthValueHistory;
+use Longyi\RewardPoints\Models\GrowthValueLevel;
+use Webkul\Admin\Http\Controllers\Controller;
+
+class GrowthValueController extends Controller
+{
+    protected $growthValueService;
+
+    public function __construct(GrowthValueService $growthValueService)
+    {
+        $this->growthValueService = $growthValueService;
+    }
+
+    public function index()
+    {
+        $customers = GrowthValueCustomer::with(['customer.group', 'levelConfig'])
+            ->orderBy('growth_value', 'desc')
+            ->paginate(20);
+
+        return view('rewardpoints::admin.growth-value.index', compact('customers'));
+    }
+
+    public function show($customerId)
+    {
+        $growthValueInfo = $this->growthValueService->getCustomerGrowthValue($customerId);
+        $history = GrowthValueHistory::where('customer_id', $customerId)
+            ->orderBy('created_at', 'desc')
+            ->paginate(20);
+        $customer = \Webkul\Customer\Models\Customer::find($customerId);
+
+        return view('rewardpoints::admin.growth-value.show', compact('growthValueInfo', 'history', 'customer'));
+    }
+
+    public function adjust($customerId)
+    {
+        if (request()->isMethod('post')) {
+            $validator = Validator::make(request()->all(), [
+                'amount' => 'required|integer',
+                'description' => 'nullable|string|max:255',
+            ]);
+
+            if ($validator->fails()) {
+                return redirect()->back()->withErrors($validator)->withInput();
+            }
+
+            try {
+                $this->growthValueService->adjustGrowthValue(
+                    $customerId,
+                    request('amount'),
+                    request('description', '管理员调整')
+                );
+
+                session()->flash('success', '成长值调整成功');
+            } catch (\Exception $e) {
+                session()->flash('error', '调整失败:' . $e->getMessage());
+            }
+
+            return redirect()->route('admin.growth-value.show', $customerId);
+        }
+
+        $customer = \Webkul\Customer\Models\Customer::find($customerId);
+        $growthValueInfo = $this->growthValueService->getCustomerGrowthValue($customerId);
+
+        return view('rewardpoints::admin.growth-value.adjust', compact('customer', 'growthValueInfo'));
+    }
+
+    public function levels()
+    {
+        $levels = $this->growthValueService->getAllLevels();
+
+        return view('rewardpoints::admin.growth-value.levels', compact('levels'));
+    }
+
+    public function saveLevel()
+    {
+        $validator = Validator::make(request()->all(), [
+            'level_code' => 'required|string|max:20|unique:mw_growth_value_levels,level_code,' . request('id'),
+            'level_name' => 'required|string|max:100',
+            'min_growth_value' => 'required|integer|min:0',
+            'max_growth_value' => 'nullable|integer|min:0',
+            'require_first_order' => 'boolean',
+            'discount_rate' => 'integer|min:0|max:100',
+            'sort_order' => 'integer|min:0',
+        ]);
+
+        if ($validator->fails()) {
+            return response()->json(['errors' => $validator->errors()], 422);
+        }
+
+        $id = request('id');
+
+        if ($id) {
+            $level = GrowthValueLevel::findOrFail($id);
+        } else {
+            $level = new GrowthValueLevel();
+        }
+
+        $level->fill(request()->only([
+            'level_code',
+            'level_name',
+            'min_growth_value',
+            'max_growth_value',
+            'require_first_order',
+            'discount_rate',
+            'icon',
+            'sort_order',
+        ]));
+
+        $benefits = request('benefits');
+        if (is_array($benefits)) {
+            $level->benefits = $benefits;
+        }
+
+        $level->is_active = request('is_active', true);
+        $level->save();
+
+        return response()->json(['success' => true, 'message' => '等级配置保存成功']);
+    }
+
+    public function deleteLevel($id)
+    {
+        $level = GrowthValueLevel::findOrFail($id);
+        $level->delete();
+
+        return response()->json(['success' => true, 'message' => '删除成功']);
+    }
+}

+ 23 - 5
packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php

@@ -4,14 +4,19 @@ namespace Longyi\RewardPoints\Listeners;
 
 use Longyi\RewardPoints\Repositories\RewardPointRepository;
 use Longyi\RewardPoints\Models\RewardActiveRule;
+use Longyi\RewardPoints\Models\RewardPointHistory;
+use Longyi\RewardPoints\Services\GrowthValueService;
 
 class OrderEvents
 {
     protected $rewardPointRepository;
 
-    public function __construct(RewardPointRepository $rewardPointRepository)
+    protected $growthValueService;
+
+    public function __construct(RewardPointRepository $rewardPointRepository, GrowthValueService $growthValueService)
     {
         $this->rewardPointRepository = $rewardPointRepository;
+        $this->growthValueService = $growthValueService;
     }
 
     public function handleOrderPlacement($order)
@@ -41,7 +46,7 @@ class OrderEvents
         }
     }
 
-     public function handleOrderCancellation($order)
+    public function handleOrderCancellation($order)
     {
         if (!$order->customer_id) {
             return;
@@ -77,11 +82,24 @@ class OrderEvents
             }
         });
     }
+
+    public function handleOrderCompletion($order)
+    {
+        if (!$order->customer_id) {
+            return;
+        }
+
+        if ($order->status !== \Webkul\Sales\Models\Order::STATUS_COMPLETED) {
+            return;
+        }
+
+        $this->growthValueService->handleOrderCompleted($order);
+    }
+
     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
+        $pointsPerCurrency = 10;
         return floor($order->grand_total * $pointsPerCurrency);
     }
 }
+

+ 43 - 0
packages/Longyi/RewardPoints/src/Models/GrowthValueCustomer.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Webkul\Customer\Models\Customer;
+
+class GrowthValueCustomer extends Model
+{
+    protected $table = 'mw_growth_value_customer';
+
+    protected $fillable = [
+        'customer_id',
+        'growth_value',
+        'growth_level',
+        'first_order_completed_at',
+        'level_updated_at',
+        'growth_value_expires_at',
+    ];
+
+    protected $casts = [
+        'customer_id' => 'integer',
+        'growth_value' => 'integer',
+        'first_order_completed_at' => 'datetime',
+        'level_updated_at' => 'datetime',
+        'growth_value_expires_at' => 'datetime',
+    ];
+
+    public function customer()
+    {
+        return $this->belongsTo(Customer::class, 'customer_id');
+    }
+
+    public function history()
+    {
+        return $this->hasMany(GrowthValueHistory::class, 'customer_id', 'customer_id');
+    }
+
+    public function levelConfig()
+    {
+        return $this->hasOne(GrowthValueLevel::class, 'level_code', 'growth_level');
+    }
+}

+ 52 - 0
packages/Longyi/RewardPoints/src/Models/GrowthValueHistory.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Webkul\Customer\Models\Customer;
+
+class GrowthValueHistory extends Model
+{
+    protected $table = 'mw_growth_value_history';
+    protected $primaryKey = 'history_id';
+
+    const EVENT_TYPE_ORDER = 'order';
+    const EVENT_TYPE_ADMIN_ADJUST = 'admin_adjust';
+    const EVENT_TYPE_EXPIRATION = 'expiration';
+
+    protected $fillable = [
+        'customer_id',
+        'order_id',
+        'order_amount',
+        'growth_value_earned',
+        'total_growth_value',
+        'level_before',
+        'level_after',
+        'event_type',
+        'description',
+        'expires_at',
+        'created_at',
+    ];
+
+    protected $casts = [
+        'customer_id' => 'integer',
+        'order_id' => 'integer',
+        'order_amount' => 'decimal:4',
+        'growth_value_earned' => 'integer',
+        'total_growth_value' => 'integer',
+        'expires_at' => 'datetime',
+        'created_at' => 'datetime',
+    ];
+
+    public $timestamps = false;
+
+    public function customer()
+    {
+        return $this->belongsTo(Customer::class, 'customer_id');
+    }
+
+    public function order()
+    {
+        return $this->belongsTo(\Webkul\Sales\Models\Order::class, 'order_id');
+    }
+}

+ 88 - 0
packages/Longyi/RewardPoints/src/Models/GrowthValueLevel.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Longyi\RewardPoints\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class GrowthValueLevel extends Model
+{
+    protected $table = 'mw_growth_value_levels';
+
+    protected $fillable = [
+        'level_code',
+        'level_name',
+        'min_growth_value',
+        'max_growth_value',
+        'require_first_order',
+        'discount_rate',
+        'benefits',
+        'icon',
+        'sort_order',
+        'is_active',
+    ];
+
+    protected $casts = [
+        'min_growth_value' => 'integer',
+        'max_growth_value' => 'integer',
+        'require_first_order' => 'boolean',
+        'discount_rate' => 'integer',
+        'sort_order' => 'integer',
+        'is_active' => 'boolean',
+    ];
+
+    public function getBenefitsAttribute($value)
+    {
+        return $value ? json_decode($value, true) : [];
+    }
+
+    public function setBenefitsAttribute($value)
+    {
+        $this->attributes['benefits'] = is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value;
+    }
+
+    /**
+     * 根据成长值和首单状态获取对应的等级
+     */
+    public static function getLevelByGrowthValue($growthValue, $hasFirstOrder = false)
+    {
+        $levels = self::where('is_active', true)
+            ->orderBy('min_growth_value', 'desc')
+            ->get();
+
+        foreach ($levels as $level) {
+            if ($level->require_first_order && !$hasFirstOrder) {
+                continue;
+            }
+
+            if ($growthValue >= $level->min_growth_value) {
+                if ($level->max_growth_value === null || $growthValue <= $level->max_growth_value) {
+                    return $level;
+                }
+            }
+        }
+
+        return self::where('level_code', 'V0')->where('is_active', true)->first();
+    }
+
+    /**
+     * 获取下一个等级
+     */
+    public static function getNextLevel($currentLevelCode, $hasFirstOrder = false)
+    {
+        $currentLevel = self::where('level_code', $currentLevelCode)->first();
+
+        if (!$currentLevel) {
+            return null;
+        }
+
+        return self::where('is_active', true)
+            ->where('min_growth_value', '>', $currentLevel->min_growth_value)
+            ->where(function($query) use ($hasFirstOrder) {
+                if (!$hasFirstOrder) {
+                    $query->where('require_first_order', false);
+                }
+            })
+            ->orderBy('min_growth_value', 'asc')
+            ->first();
+    }
+}

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

@@ -19,5 +19,9 @@ class EventServiceProvider extends ServiceProvider
         'customer.review.create.after' => [
             'Longyi\RewardPoints\Listeners\ReviewEvents@handleReviewCreation'
         ],
+        'sales.order.complete.after' => [
+            'Longyi\RewardPoints\Listeners\OrderEvents@handleOrderCompletion'
+        ],
     ];
 }
+

+ 19 - 17
packages/Longyi/RewardPoints/src/Providers/RewardPointsServiceProvider.php

@@ -16,7 +16,7 @@ class RewardPointsServiceProvider extends ServiceProvider
         $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');
@@ -39,19 +39,21 @@ class RewardPointsServiceProvider extends ServiceProvider
         $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,
+                \Longyi\RewardPoints\Console\Commands\InitializeGrowthSettings::class,
+                \Longyi\RewardPoints\Console\Commands\CheckExpiredGrowthValues::class,
             ]);
         }
     }
@@ -62,39 +64,39 @@ class RewardPointsServiceProvider extends ServiceProvider
     {
         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);
         }
     }
-    
+
     /**
      * 将代码转换为配置键
      */
@@ -117,10 +119,10 @@ class RewardPointsServiceProvider extends ServiceProvider
             'referral_require_order' => 'rewardpoints.referral.require_first_order',
             'birthday_points' => 'rewardpoints.birthday.points_per_birthday',
         ];
-        
+
         return $mapping[$code] ?? null;
     }
-    
+
     /**
      * 类型转换
      */
@@ -129,7 +131,7 @@ class RewardPointsServiceProvider extends ServiceProvider
         if ($value === null) {
             return null;
         }
-        
+
         switch ($type) {
             case 'boolean':
                 return (bool) $value;
@@ -139,5 +141,5 @@ class RewardPointsServiceProvider extends ServiceProvider
                 return $value;
         }
     }
-    
-}
+
+}

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

@@ -166,4 +166,12 @@ Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin/reward-points
         'as' => 'admin.reward-points.transactions.export',
         'uses' => 'Longyi\RewardPoints\Http\Controllers\Admin\TransactionController@export'
     ]);
+    //成长值
+    Route::get('/growth-value', [GrowthValueController::class, 'index'])->name('admin.growth-value.index');
+    Route::get('/growth-value/{customerId}', [GrowthValueController::class, 'show'])->name('admin.growth-value.show');
+    Route::post('/growth-value/{customerId}/adjust', [GrowthValueController::class, 'adjust'])->name('admin.growth-value.adjust');
+
+    Route::get('/growth-value-levels', [GrowthValueController::class, 'levels'])->name('admin.growth-value.levels');
+    Route::post('/growth-value-levels/save', [GrowthValueController::class, 'saveLevel'])->name('admin.growth-value.levels.save');
+    Route::delete('/growth-value-levels/{id}', [GrowthValueController::class, 'deleteLevel'])->name('admin.growth-value.levels.delete');
 });

+ 319 - 0
packages/Longyi/RewardPoints/src/Services/GrowthValueService.php

@@ -0,0 +1,319 @@
+<?php
+
+namespace Longyi\RewardPoints\Services;
+
+use Longyi\RewardPoints\Models\GrowthValueCustomer;
+use Longyi\RewardPoints\Models\GrowthValueHistory;
+use Longyi\RewardPoints\Models\GrowthValueLevel;
+use Longyi\RewardPoints\Models\RewardPointSetting;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class GrowthValueService
+{
+    /**
+     * 处理订单完成事件,增加成长值并检查升级
+     */
+    public function handleOrderCompleted($order)
+    {
+        if (!$order->customer_id) {
+            return;
+        }
+
+        try {
+            DB::transaction(function () use ($order) {
+                $customerId = $order->customer_id;
+                $orderAmount = $order->grand_total;
+
+                $growthValueCustomer = GrowthValueCustomer::firstOrCreate(
+                    ['customer_id' => $customerId],
+                    [
+                        'growth_value' => 0,
+                        'growth_level' => 'V0',
+                        'level_updated_at' => now(),
+                    ]
+                );
+
+                $levelBefore = $growthValueCustomer->growth_level;
+                $hasFirstOrder = !is_null($growthValueCustomer->first_order_completed_at);
+
+                if (!$hasFirstOrder) {
+                    $growthValueCustomer->first_order_completed_at = now();
+                }
+
+                $earnedGrowthValue = $this->calculateGrowthValue($orderAmount);
+
+                $currentGrowthValue = $growthValueCustomer->growth_value;
+                $newGrowthValue = $currentGrowthValue + $earnedGrowthValue;
+
+                $validityDays = $this->getValidityDays();
+                $expiresAt = $validityDays > 0 ? Carbon::now()->addDays($validityDays) : null;
+
+                $growthValueCustomer->growth_value = $newGrowthValue;
+
+                if ($validityDays > 0) {
+                    $growthValueCustomer->growth_value_expires_at = $expiresAt;
+                }
+
+                $newLevel = GrowthValueLevel::getLevelByGrowthValue($newGrowthValue, true);
+                $levelAfter = $newLevel ? $newLevel->level_code : 'V0';
+
+                if ($levelBefore !== $levelAfter) {
+                    $growthValueCustomer->growth_level = $levelAfter;
+                    $growthValueCustomer->level_updated_at = now();
+                }
+
+                $growthValueCustomer->save();
+
+                GrowthValueHistory::create([
+                    'customer_id' => $customerId,
+                    'order_id' => $order->id,
+                    'order_amount' => $orderAmount,
+                    'growth_value_earned' => $earnedGrowthValue,
+                    'total_growth_value' => $newGrowthValue,
+                    'level_before' => $levelBefore,
+                    'level_after' => $levelAfter,
+                    'event_type' => GrowthValueHistory::EVENT_TYPE_ORDER,
+                    'description' => "订单 #{$order->increment_id} 完成,获得 {$earnedGrowthValue} 成长值",
+                    'expires_at' => $expiresAt,
+                    'created_at' => now(),
+                ]);
+
+                Log::info('Growth value added and level checked', [
+                    'customer_id' => $customerId,
+                    'order_id' => $order->id,
+                    'order_amount' => $orderAmount,
+                    'earned_growth_value' => $earnedGrowthValue,
+                    'new_total' => $newGrowthValue,
+                    'level_before' => $levelBefore,
+                    'level_after' => $levelAfter,
+                ]);
+            });
+        } catch (\Exception $e) {
+            Log::error('Error adding growth value', [
+                'customer_id' => $order->customer_id,
+                'order_id' => $order->id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+        }
+    }
+
+    /**
+     * 计算订单应获得的成长值
+     */
+    protected function calculateGrowthValue($orderAmount)
+    {
+        $ratio = $this->getGrowthValueRatio();
+
+        return max(1, floor($orderAmount * $ratio));
+    }
+
+    /**
+     * 获取成长值计算比例(每元获得的成长值)
+     */
+    protected function getGrowthValueRatio()
+    {
+        $setting = RewardPointSetting::where('code', 'growth_value_ratio')->first();
+
+        if ($setting && $setting->value) {
+            return floatval($setting->value);
+        }
+
+        return 1.0;
+    }
+
+    /**
+     * 获取成长值有效期(天)
+     */
+    protected function getValidityDays()
+    {
+        $setting = RewardPointSetting::where('code', 'growth_value_validity_days')->first();
+
+        if ($setting && $setting->value) {
+            return intval($setting->value);
+        }
+
+        return 365;
+    }
+
+    /**
+     * 检查并处理过期的成长值
+     */
+    public function checkExpiredGrowthValues()
+    {
+        $expiredRecords = GrowthValueCustomer::whereNotNull('growth_value_expires_at')
+            ->where('growth_value_expires_at', '<', now())
+            ->where('growth_value', '>', 0)
+            ->get();
+
+        foreach ($expiredRecords as $record) {
+            try {
+                DB::transaction(function () use ($record) {
+                    $levelBefore = $record->growth_level;
+
+                    GrowthValueHistory::create([
+                        'customer_id' => $record->customer_id,
+                        'order_id' => null,
+                        'order_amount' => 0,
+                        'growth_value_earned' => -$record->growth_value,
+                        'total_growth_value' => 0,
+                        'level_before' => $levelBefore,
+                        'level_after' => 'V0',
+                        'event_type' => GrowthValueHistory::EVENT_TYPE_EXPIRATION,
+                        'description' => '成长值过期清零',
+                        'expires_at' => null,
+                        'created_at' => now(),
+                    ]);
+
+                    $record->growth_value = 0;
+                    $record->growth_level = 'V0';
+                    $record->level_updated_at = now();
+                    $record->growth_value_expires_at = null;
+                    $record->save();
+
+                    Log::info('Growth value expired and level reset', [
+                        'customer_id' => $record->customer_id,
+                        'expired_value' => $record->growth_value,
+                        'level_before' => $levelBefore,
+                    ]);
+                });
+            } catch (\Exception $e) {
+                Log::error('Error processing expired growth value', [
+                    'customer_id' => $record->customer_id,
+                    'error' => $e->getMessage(),
+                ]);
+            }
+        }
+    }
+
+    /**
+     * 获取客户成长值信息
+     */
+    public function getCustomerGrowthValue($customerId)
+    {
+        $growthValueCustomer = GrowthValueCustomer::with('levelConfig')->where('customer_id', $customerId)->first();
+        $customer = \Webkul\Customer\Models\Customer::find($customerId);
+
+        if (!$growthValueCustomer) {
+            $defaultLevel = GrowthValueLevel::where('level_code', 'V0')->first();
+            return [
+                'growth_value' => 0,
+                'growth_level' => 'V0',
+                'level_name' => $defaultLevel ? $defaultLevel->level_name : '普通会员',
+                'level_icon' => $defaultLevel ? $defaultLevel->icon : null,
+                'discount_rate' => $defaultLevel ? $defaultLevel->discount_rate : 0,
+                'benefits' => $defaultLevel ? $defaultLevel->benefits : [],
+                'next_level' => null,
+                'points_to_next_level' => null,
+                'expires_at' => null,
+                'first_order_completed' => false,
+                'customer_group' => $customer ? $customer->group : null,
+            ];
+        }
+
+        $levelConfig = $growthValueCustomer->levelConfig;
+
+        $nextLevel = GrowthValueLevel::getNextLevel(
+            $growthValueCustomer->growth_level,
+            !is_null($growthValueCustomer->first_order_completed_at)
+        );
+
+        return [
+            'growth_value' => $growthValueCustomer->growth_value,
+            'growth_level' => $growthValueCustomer->growth_level,
+            'level_name' => $levelConfig ? $levelConfig->level_name : '普通会员',
+            'level_icon' => $levelConfig ? $levelConfig->icon : null,
+            'discount_rate' => $levelConfig ? $levelConfig->discount_rate : 0,
+            'benefits' => $levelConfig ? $levelConfig->benefits : [],
+            'next_level' => $nextLevel ? [
+                'level_code' => $nextLevel->level_code,
+                'level_name' => $nextLevel->level_name,
+                'min_growth_value' => $nextLevel->min_growth_value,
+                'icon' => $nextLevel->icon,
+            ] : null,
+            'points_to_next_level' => $nextLevel ? ($nextLevel->min_growth_value - $growthValueCustomer->growth_value) : null,
+            'expires_at' => $growthValueCustomer->growth_value_expires_at,
+            'first_order_completed' => !is_null($growthValueCustomer->first_order_completed_at),
+            'customer_group' => $customer ? $customer->group : null,
+            'level_updated_at' => $growthValueCustomer->level_updated_at,
+        ];
+    }
+
+    /**
+     * 手动调整成长值(管理员操作)
+     */
+    public function adjustGrowthValue($customerId, $amount, $description = '管理员调整')
+    {
+        try {
+            return DB::transaction(function () use ($customerId, $amount, $description) {
+                $growthValueCustomer = GrowthValueCustomer::firstOrCreate(
+                    ['customer_id' => $customerId],
+                    [
+                        'growth_value' => 0,
+                        'growth_level' => 'V0',
+                        'level_updated_at' => now(),
+                    ]
+                );
+
+                $levelBefore = $growthValueCustomer->growth_level;
+                $currentGrowthValue = $growthValueCustomer->growth_value;
+                $newGrowthValue = max(0, $currentGrowthValue + $amount);
+
+                $hasFirstOrder = !is_null($growthValueCustomer->first_order_completed_at);
+                $newLevel = GrowthValueLevel::getLevelByGrowthValue($newGrowthValue, $hasFirstOrder);
+                $levelAfter = $newLevel ? $newLevel->level_code : 'V0';
+
+                if ($levelBefore !== $levelAfter) {
+                    $growthValueCustomer->growth_level = $levelAfter;
+                    $growthValueCustomer->level_updated_at = now();
+                }
+
+                $growthValueCustomer->growth_value = $newGrowthValue;
+                $growthValueCustomer->save();
+
+                $history = GrowthValueHistory::create([
+                    'customer_id' => $customerId,
+                    'order_id' => null,
+                    'order_amount' => 0,
+                    'growth_value_earned' => $amount,
+                    'total_growth_value' => $newGrowthValue,
+                    'level_before' => $levelBefore,
+                    'level_after' => $levelAfter,
+                    'event_type' => GrowthValueHistory::EVENT_TYPE_ADMIN_ADJUST,
+                    'description' => $description,
+                    'expires_at' => null,
+                    'created_at' => now(),
+                ]);
+
+                Log::info('Growth value adjusted by admin', [
+                    'customer_id' => $customerId,
+                    'amount' => $amount,
+                    'new_total' => $newGrowthValue,
+                    'level_before' => $levelBefore,
+                    'level_after' => $levelAfter,
+                ]);
+
+                return $history;
+            });
+        } catch (\Exception $e) {
+            Log::error('Error adjusting growth value', [
+                'customer_id' => $customerId,
+                'amount' => $amount,
+                'error' => $e->getMessage(),
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * 获取所有等级配置
+     */
+    public function getAllLevels()
+    {
+        return GrowthValueLevel::where('is_active', true)
+            ->orderBy('sort_order')
+            ->get();
+    }
+}