Преглед изворни кода

feat(shop): 新增促销活动功能

- 添加促销活动模型、控制器和服务
- 实现促销活动的创建、编辑、删除和查询
- 增加促销商品管理功能,支持自定义折扣
- 优化商品查询接口,支持促销活动过滤
- 新增商品折扣价格计算方法
notfff пре 8 месеци
родитељ
комит
3a6feacbf1

+ 25 - 1
app/Module/AppGame/Handler/Shop/QueryHandler.php

@@ -37,7 +37,9 @@ class QueryHandler extends BaseHandler
 
         try {
             // 获取请求参数
-            $categoryId = $data->getCategoryId();
+            $categoryId = $data->getCategoryId() ?? 0;
+            $promotionId = $data->getPromotionId() ?? 0;
+            $onlyPromotion = $data->getOnlyPromotion() ?? false;
             $userId = $this->user_id;
 
             // 构建过滤条件
@@ -46,6 +48,14 @@ class QueryHandler extends BaseHandler
                 $filters['category_id'] = $categoryId;
             }
 
+            if ($promotionId > 0) {
+                $filters['promotion_id'] = $promotionId;
+            }
+
+            if ($onlyPromotion) {
+                $filters['only_promotion'] = true;
+            }
+
             // 获取商店商品列表
             $shopItems = ShopService::getShopItems($filters);
 
@@ -70,6 +80,20 @@ class QueryHandler extends BaseHandler
                 $dataItem->setMaxBuy($shopItem->max_buy);
                 $dataItem->setImage($shopItem->image);
 
+                // 设置折扣价格信息
+                $dataItem->setOriginalPrice($shopItem->original_price ?? $shopItem->price);
+                $dataItem->setDiscountedPrice($shopItem->discounted_price ?? $shopItem->price);
+                $dataItem->setHasDiscount($shopItem->has_discount ?? false);
+                $dataItem->setDiscountPercentage($shopItem->discount_percentage ?? 0);
+
+                // 如果有促销活动,设置促销信息
+                $promotion = $shopItem->getActivePromotion();
+                if ($promotion) {
+                    $dataItem->setPromotionId($promotion->id);
+                    $dataItem->setPromotionName($promotion->name);
+                    $dataItem->setPromotionEndTime($promotion->end_time ? $promotion->end_time->timestamp : 0);
+                }
+
                 // 如果有购买限制,获取用户已购买数量
                 if ($shopItem->max_buy > 0) {
                     $boughtCount = $shopItem->getUserBoughtCount($userId);

+ 53 - 0
app/Module/Farm/Events/HouseDowngradedEvent.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Module\Farm\Events;
+
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+/**
+ * 房屋降级事件
+ *
+ * 当房屋等级降低时触发此事件
+ */
+class HouseDowngradedEvent
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    /**
+     * 用户ID
+     *
+     * @var int
+     */
+    public $userId;
+
+    /**
+     * 旧等级
+     *
+     * @var int
+     */
+    public $oldLevel;
+
+    /**
+     * 新等级
+     *
+     * @var int
+     */
+    public $newLevel;
+
+    /**
+     * 创建一个新的事件实例
+     *
+     * @param int $userId 用户ID
+     * @param int $oldLevel 旧等级
+     * @param int $newLevel 新等级
+     * @return void
+     */
+    public function __construct(int $userId, int $oldLevel, int $newLevel)
+    {
+        $this->userId = $userId;
+        $this->oldLevel = $oldLevel;
+        $this->newLevel = $newLevel;
+    }
+}

+ 25 - 2
app/Module/Game/Dtos/HouseChangeTempDto.php

@@ -56,10 +56,29 @@ class HouseChangeTempDto extends BaseDto
     /**
      * 从缓存数据创建DTO对象
      *
-     * @param array|null $cachedData 缓存数据
+     * @param mixed $cachedData 缓存数据
+     * @return array 包含DTO对象的数组
+     */
+    public static function fromCache($cachedData): array
+    {
+        // 为了保持与原有逻辑兼容,我们将创建一个新的方法来处理单个对象的情况
+        $singleDto = self::fromCacheSingle($cachedData);
+        if ($singleDto === null) {
+            return [];
+        }
+
+        return [0 => $singleDto]; // 使用0作为默认键
+    }
+
+    /**
+     * 从缓存数据创建单个DTO对象
+     *
+     * 这是一个辅助方法,保留原有的逻辑,但不覆盖父类的fromCache方法
+     *
+     * @param mixed $cachedData 缓存数据
      * @return HouseChangeTempDto|null 包含DTO对象或null
      */
-    public static function fromCache(?array $cachedData): ?HouseChangeTempDto
+    public static function fromCacheSingle($cachedData): ?HouseChangeTempDto
     {
         if (empty($cachedData)) {
             return null;
@@ -69,6 +88,10 @@ class HouseChangeTempDto extends BaseDto
             return $cachedData;
         }
 
+        if (!is_array($cachedData)) {
+            return null;
+        }
+
         $dto = new self();
         $dto->oldLevel = $cachedData['old_level'] ?? 0;
         $dto->newLevel = $cachedData['new_level'] ?? 0;

+ 6 - 2
app/Module/Game/Dtos/LandChangeTempDto.php

@@ -49,11 +49,15 @@ class LandChangeTempDto extends BaseDto
     /**
      * 从缓存数据创建DTO对象
      *
-     * @param array $cachedData 缓存数据
+     * @param mixed $cachedData 缓存数据
      * @return array 包含DTO对象的数组
      */
-    public static function fromCache(array $cachedData): array
+    public static function fromCache($cachedData): array
     {
+        if (!is_array($cachedData)) {
+            return [];
+        }
+
         $result = [];
         foreach ($cachedData as $landId => $data) {
             if (is_array($data)) {

+ 6 - 2
app/Module/Game/Dtos/LandStatusTempDto.php

@@ -63,11 +63,15 @@ class LandStatusTempDto extends BaseDto
     /**
      * 从缓存数据创建DTO对象
      *
-     * @param array $cachedData 缓存数据
+     * @param mixed $cachedData 缓存数据
      * @return array 包含DTO对象的数组
      */
-    public static function fromCache(array $cachedData): array
+    public static function fromCache($cachedData): array
     {
+        if (!is_array($cachedData)) {
+            return [];
+        }
+
         $result = [];
         foreach ($cachedData as $landId => $data) {
             if (is_array($data)) {

+ 6 - 7
app/Module/Game/Logics/HouseTemp.php

@@ -2,7 +2,6 @@
 
 namespace App\Module\Game\Logics;
 
-use App\Module\Farm\Events\HouseDowngradedEvent;
 use App\Module\Farm\Events\HouseUpgradedEvent;
 use App\Module\Farm\Services\HouseService;
 use App\Module\Game\Dtos\HouseChangeTempDto;
@@ -44,10 +43,10 @@ class HouseTemp
     /**
      * 处理房屋降级事件
      *
-     * @param HouseDowngradedEvent $event 房屋降级事件
+     * @param \App\Module\Farm\Events\HouseDowngradedEvent $event 房屋降级事件
      * @return void
      */
-    public static function handleHouseDowngraded(HouseDowngradedEvent $event): void
+    public static function handleHouseDowngraded(\App\Module\Farm\Events\HouseDowngradedEvent $event): void
     {
         self::handleHouseChange($event->userId, $event->oldLevel, $event->newLevel, false);
     }
@@ -68,15 +67,15 @@ class HouseTemp
             $tempKey = self::TEMP_KEY_PREFIX . $userId;
 
             // 获取房屋配置信息
-            $houseConfig = HouseService::getHouseConfigByLevel($newLevel);
+            $houseConfig = HouseService::getHouseConfig($newLevel);
 
             // 构建房屋变更数据
             $houseData = [
                 'old_level' => $oldLevel,
                 'new_level' => $newLevel,
                 'is_upgrade' => $isUpgrade,
-                'output_bonus' => $houseConfig['output_bonus'] ?? 0.0,
-                'special_land_limit' => $houseConfig['special_land_limit'] ?? 0,
+                'output_bonus' => $houseConfig ? $houseConfig->output_bonus : 0.0,
+                'special_land_limit' => $houseConfig ? $houseConfig->special_land_limit : 0,
                 'updated_at' => time(),
             ];
 
@@ -110,7 +109,7 @@ class HouseTemp
         $tempKey = self::TEMP_KEY_PREFIX . $userId;
         $cachedData = Cache::get($tempKey);
 
-        return HouseChangeTempDto::fromCache($cachedData);
+        return HouseChangeTempDto::fromCacheSingle($cachedData);
     }
 
     /**

+ 320 - 0
app/Module/Shop/AdminControllers/ShopPromotionController.php

@@ -0,0 +1,320 @@
+<?php
+
+namespace App\Module\Shop\AdminControllers;
+
+use App\Module\Shop\AdminControllers\Helper\FilterHelper;
+use App\Module\Shop\AdminControllers\Helper\FormHelper;
+use App\Module\Shop\AdminControllers\Helper\GridHelper;
+use App\Module\Shop\AdminControllers\Helper\ShowHelper;
+use App\Module\Shop\Models\ShopItem;
+use App\Module\Shop\Models\ShopPromotion;
+use App\Module\Shop\Repositorys\ShopPromotionRepository;
+use Dcat\Admin\Form;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Show;
+use Dcat\Admin\Http\Controllers\AdminController;
+use Dcat\Admin\Layout\Content;
+use Dcat\Admin\Widgets\Card;
+use Dcat\Admin\Widgets\Table;
+use Illuminate\Http\Request;
+use Spatie\RouteAttributes\Attributes\Resource;
+use Spatie\RouteAttributes\Attributes\Get;
+use Spatie\RouteAttributes\Attributes\Post;
+
+/**
+ * 商店促销活动管理控制器
+ */
+#[Resource('shop/promotions', names: 'dcat.admin.shop.promotions')]
+class ShopPromotionController extends AdminController
+{
+    /**
+     * 页面标题
+     *
+     * @var string
+     */
+    protected $title = '促销活动';
+
+    /**
+     * 创建表格
+     *
+     * @return Grid
+     */
+    protected function grid()
+    {
+        return Grid::make(new ShopPromotionRepository(), function (Grid $grid) {
+            $grid->column('id', 'ID')->sortable();
+            $grid->column('name', '活动名称');
+            $grid->column('banner', '横幅图片')->image('', 100, 50);
+            $grid->column('discount_type', '折扣类型')->display(function ($type) {
+                return $type == ShopPromotion::DISCOUNT_TYPE_FIXED ? '固定折扣' : '百分比折扣';
+            });
+            $grid->column('discount_value', '折扣值')->display(function ($value) {
+                if ($this->discount_type == ShopPromotion::DISCOUNT_TYPE_FIXED) {
+                    return $value . ' 元';
+                } else {
+                    return $value . '%';
+                }
+            });
+            $grid->column('is_active', '状态')->switch();
+            $grid->column('sort_order', '排序权重')->sortable();
+            $grid->column('start_time', '开始时间')->sortable();
+            $grid->column('end_time', '结束时间')->sortable();
+            $grid->column('created_at', '创建时间')->sortable();
+
+            // 添加操作按钮
+            $grid->actions(function (Grid\Displayers\Actions $actions) {
+                $actions->append('<a href="' . admin_url('shop/promotions/' . $actions->row->id . '/items') . '" class="btn btn-sm btn-primary">管理商品</a>');
+            });
+
+            // 过滤器
+            $grid->filter(function (Grid\Filter $filter) {
+                $filter->equal('id', 'ID');
+                $filter->like('name', '活动名称');
+                $filter->equal('discount_type', '折扣类型')->select([
+                    ShopPromotion::DISCOUNT_TYPE_FIXED => '固定折扣',
+                    ShopPromotion::DISCOUNT_TYPE_PERCENTAGE => '百分比折扣',
+                ]);
+                $filter->equal('is_active', '状态')->select([
+                    0 => '禁用',
+                    1 => '启用',
+                ]);
+                $filter->between('start_time', '开始时间')->datetime();
+                $filter->between('end_time', '结束时间')->datetime();
+            });
+
+            // 工具栏
+            $grid->toolsWithOutline(false);
+            $grid->disableViewButton();
+            $grid->showQuickEditButton();
+            $grid->enableDialogCreate();
+            $grid->enableDialogEdit();
+            $grid->setDialogFormDimensions('800px', '720px');
+        });
+    }
+
+    /**
+     * 创建详情页
+     *
+     * @param mixed $id
+     * @return Show
+     */
+    protected function detail($id)
+    {
+        return Show::make($id, new ShopPromotionRepository(), function (Show $show) {
+            $show->field('id', 'ID');
+            $show->field('name', '活动名称');
+            $show->field('description', '活动描述');
+            $show->field('banner', '横幅图片')->image();
+            $show->field('discount_type', '折扣类型')->as(function ($type) {
+                return $type == ShopPromotion::DISCOUNT_TYPE_FIXED ? '固定折扣' : '百分比折扣';
+            });
+            $show->field('discount_value', '折扣值')->as(function ($value) {
+                if ($this->discount_type == ShopPromotion::DISCOUNT_TYPE_FIXED) {
+                    return $value . ' 元';
+                } else {
+                    return $value . '%';
+                }
+            });
+            $show->field('is_active', '状态')->as(function ($isActive) {
+                return $isActive ? '启用' : '禁用';
+            });
+            $show->field('sort_order', '排序权重');
+            $show->field('start_time', '开始时间');
+            $show->field('end_time', '结束时间');
+            $show->field('created_at', '创建时间');
+            $show->field('updated_at', '更新时间');
+        });
+    }
+
+    /**
+     * 创建表单
+     *
+     * @return Form
+     */
+    protected function form()
+    {
+        return Form::make(new ShopPromotionRepository(), function (Form $form) {
+            $form->display('id', 'ID');
+            $form->text('name', '活动名称')->required();
+            $form->textarea('description', '活动描述')->rows(3);
+            $form->image('banner', '横幅图片')->autoUpload()->uniqueName()->help('建议尺寸:1200x300');
+            $form->radio('discount_type', '折扣类型')
+                ->options([
+                    ShopPromotion::DISCOUNT_TYPE_FIXED => '固定折扣',
+                    ShopPromotion::DISCOUNT_TYPE_PERCENTAGE => '百分比折扣',
+                ])
+                ->default(ShopPromotion::DISCOUNT_TYPE_PERCENTAGE)
+                ->required();
+            $form->number('discount_value', '折扣值')
+                ->min(0)
+                ->help('固定折扣为具体金额,百分比折扣为1-100的整数')
+                ->required();
+            $form->number('sort_order', '排序权重')->default(0)->help('数字越小越靠前');
+            $form->switch('is_active', '状态')->default(true);
+            $form->datetime('start_time', '开始时间')->help('留空表示不限制开始时间');
+            $form->datetime('end_time', '结束时间')->help('留空表示不限制结束时间');
+            
+            $form->display('created_at', '创建时间');
+            $form->display('updated_at', '更新时间');
+        });
+    }
+
+    /**
+     * 管理促销活动商品
+     *
+     * @param Content $content
+     * @param int $id
+     * @return Content
+     */
+    #[Get('shop/promotions/{id}/items')]
+    public function items(Content $content, $id)
+    {
+        $promotion = ShopPromotion::findOrFail($id);
+        
+        $content->title('管理促销活动商品');
+        $content->description('为促销活动 "' . $promotion->name . '" 添加或移除商品');
+        
+        // 已添加的商品
+        $addedItems = $promotion->items()->with('category')->get();
+        $addedItemsTable = $this->buildAddedItemsTable($addedItems, $promotion);
+        
+        // 添加商品表单
+        $addItemForm = $this->buildAddItemForm($id);
+        
+        $content->row(function ($row) use ($promotion) {
+            $row->column(12, new Card('促销活动信息', view('admin.shop.promotion_info', ['promotion' => $promotion])));
+        });
+        
+        $content->row(function ($row) use ($addedItemsTable) {
+            $row->column(12, new Card('已添加商品', $addedItemsTable));
+        });
+        
+        $content->row(function ($row) use ($addItemForm) {
+            $row->column(12, new Card('添加商品', $addItemForm));
+        });
+        
+        return $content;
+    }
+    
+    /**
+     * 构建已添加商品表格
+     *
+     * @param Collection $items
+     * @param ShopPromotion $promotion
+     * @return Table
+     */
+    protected function buildAddedItemsTable($items, $promotion)
+    {
+        $headers = ['ID', '商品名称', '分类', '原价', '折扣价', '自定义折扣值', '操作'];
+        $rows = [];
+        
+        foreach ($items as $item) {
+            $customDiscountValue = $item->pivot->custom_discount_value;
+            $originalPrice = $item->price;
+            $discountedPrice = $promotion->calculateDiscountedPrice($originalPrice, $customDiscountValue);
+            
+            $removeUrl = admin_url('shop/promotions/' . $promotion->id . '/items/remove?item_id=' . $item->id);
+            $removeButton = "<a href=\"{$removeUrl}\" class=\"btn btn-sm btn-danger\" onclick=\"return confirm('确定要移除该商品吗?')\">移除</a>";
+            
+            $rows[] = [
+                $item->id,
+                $item->name,
+                $item->category->name ?? '未分类',
+                $originalPrice,
+                $discountedPrice,
+                $customDiscountValue ?? '使用默认值',
+                $removeButton
+            ];
+        }
+        
+        return new Table($headers, $rows);
+    }
+    
+    /**
+     * 构建添加商品表单
+     *
+     * @param int $promotionId
+     * @return string
+     */
+    protected function buildAddItemForm($promotionId)
+    {
+        $form = new Form();
+        $form->action(admin_url('shop/promotions/' . $promotionId . '/items/add'));
+        
+        $form->select('item_id', '选择商品')
+            ->options(ShopItem::where('is_active', true)->pluck('name', 'id'))
+            ->required();
+            
+        $form->number('custom_discount_value', '自定义折扣值')
+            ->help('留空则使用促销活动的默认折扣值');
+            
+        $form->hidden('_token')->default(csrf_token());
+        
+        $form->submit('添加商品');
+        
+        return $form->render();
+    }
+    
+    /**
+     * 添加商品到促销活动
+     *
+     * @param Request $request
+     * @param int $id
+     * @return \Illuminate\Http\RedirectResponse
+     */
+    #[Post('shop/promotions/{id}/items/add')]
+    public function addItem(Request $request, $id)
+    {
+        $request->validate([
+            'item_id' => 'required|integer|exists:shop_items,id',
+            'custom_discount_value' => 'nullable|integer|min:0',
+        ]);
+        
+        $promotionId = $id;
+        $shopItemId = $request->input('item_id');
+        $customDiscountValue = $request->input('custom_discount_value');
+        
+        // 添加商品到促销活动
+        $result = \App\Module\Shop\Services\ShopService::addItemToPromotion(
+            $promotionId,
+            $shopItemId,
+            $customDiscountValue
+        );
+        
+        admin_success('成功', '商品已添加到促销活动');
+        
+        return redirect()->back();
+    }
+    
+    /**
+     * 从促销活动中移除商品
+     *
+     * @param Request $request
+     * @param int $id
+     * @return \Illuminate\Http\RedirectResponse
+     */
+    #[Get('shop/promotions/{id}/items/remove')]
+    public function removeItem(Request $request, $id)
+    {
+        $request->validate([
+            'item_id' => 'required|integer|exists:shop_items,id',
+        ]);
+        
+        $promotionId = $id;
+        $shopItemId = $request->input('item_id');
+        
+        // 从促销活动中移除商品
+        $result = \App\Module\Shop\Services\ShopService::removeItemFromPromotion(
+            $promotionId,
+            $shopItemId
+        );
+        
+        if ($result) {
+            admin_success('成功', '商品已从促销活动中移除');
+        } else {
+            admin_error('失败', '移除商品失败');
+        }
+        
+        return redirect()->back();
+    }
+}

+ 35 - 0
app/Module/Shop/Databases/create.sql

@@ -61,3 +61,38 @@ CREATE TABLE IF NOT EXISTS `kku_shop_purchase_logs` (
   KEY `shop_purchase_logs_item_id_index` (`item_id`),
   KEY `shop_purchase_logs_purchase_time_index` (`purchase_time`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商店购买记录表';
+
+-- 创建商店促销活动表
+CREATE TABLE IF NOT EXISTS `kku_shop_promotions` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '促销ID,主键',
+  `name` varchar(100) NOT NULL COMMENT '促销名称',
+  `description` text COMMENT '促销描述',
+  `banner` varchar(255) DEFAULT NULL COMMENT '促销横幅图片',
+  `discount_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '折扣类型(1:固定折扣, 2:百分比折扣)',
+  `discount_value` int(11) NOT NULL COMMENT '折扣值(固定折扣为具体金额,百分比折扣为1-100的整数)',
+  `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否激活(0:否, 1:是)',
+  `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序权重',
+  `start_time` timestamp NULL DEFAULT NULL COMMENT '开始时间',
+  `end_time` timestamp NULL DEFAULT NULL COMMENT '结束时间',
+  `created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `shop_promotions_is_active_index` (`is_active`),
+  KEY `shop_promotions_sort_order_index` (`sort_order`),
+  KEY `shop_promotions_start_time_index` (`start_time`),
+  KEY `shop_promotions_end_time_index` (`end_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商店促销活动表';
+
+-- 创建商店促销商品关联表
+CREATE TABLE IF NOT EXISTS `kku_shop_promotion_items` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '关联ID,主键',
+  `promotion_id` int(10) unsigned NOT NULL COMMENT '促销ID,外键关联kku_shop_promotions表',
+  `shop_item_id` int(10) unsigned NOT NULL COMMENT '商品ID,外键关联kku_shop_items表',
+  `custom_discount_value` int(11) DEFAULT NULL COMMENT '自定义折扣值(可为空,优先于促销活动的折扣值)',
+  `created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `shop_promotion_items_promotion_id_shop_item_id_unique` (`promotion_id`,`shop_item_id`),
+  KEY `shop_promotion_items_promotion_id_index` (`promotion_id`),
+  KEY `shop_promotion_items_shop_item_id_index` (`shop_item_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商店促销商品关联表';

+ 167 - 2
app/Module/Shop/Logics/ShopLogic.php

@@ -29,7 +29,19 @@ class ShopLogic
      */
     public function getShopItems(array $filters = []): Collection
     {
-        $query = ShopItem::with(['item', 'category'])
+        $query = ShopItem::with(['item', 'category', 'promotions' => function ($q) {
+                $now = Carbon::now();
+                $q->where('is_active', true)
+                    ->where(function ($q) use ($now) {
+                        $q->whereNull('start_time')
+                            ->orWhere('start_time', '<=', $now);
+                    })
+                    ->where(function ($q) use ($now) {
+                        $q->whereNull('end_time')
+                            ->orWhere('end_time', '>=', $now);
+                    })
+                    ->orderBy('sort_order', 'asc');
+            }])
             ->where('is_active', true)
             ->where(function ($q) {
                 $now = Carbon::now();
@@ -68,11 +80,47 @@ class ShopLogic
             });
         }
 
+        // 是否只获取促销商品
+        if (isset($filters['only_promotion']) && $filters['only_promotion']) {
+            $query->whereHas('promotions', function ($q) {
+                $now = Carbon::now();
+                $q->where('is_active', true)
+                    ->where(function ($q) use ($now) {
+                        $q->whereNull('start_time')
+                            ->orWhere('start_time', '<=', $now);
+                    })
+                    ->where(function ($q) use ($now) {
+                        $q->whereNull('end_time')
+                            ->orWhere('end_time', '>=', $now);
+                    });
+            });
+        }
+
+        // 按促销活动过滤
+        if (isset($filters['promotion_id'])) {
+            $query->whereHas('promotions', function ($q) use ($filters) {
+                $q->where('id', $filters['promotion_id']);
+            });
+        }
+
         // 排序
         $query->orderBy('sort_order', 'asc')
             ->orderBy('id', 'asc');
 
-        return $query->get();
+        // 获取商品列表
+        $items = $query->get();
+
+        // 计算折扣价格
+        foreach ($items as $item) {
+            $item->original_price = $item->price;
+            $item->discounted_price = $item->getDiscountedPrice();
+            $item->has_discount = $item->discounted_price < $item->original_price;
+            $item->discount_percentage = $item->original_price > 0
+                ? round((1 - $item->discounted_price / $item->original_price) * 100)
+                : 0;
+        }
+
+        return $items;
     }
 
     /**
@@ -195,4 +243,121 @@ class ShopLogic
 
         return $query->get();
     }
+
+    /**
+     * 获取促销活动列表
+     *
+     * @param array $filters 过滤条件
+     * @return Collection 促销活动列表
+     */
+    public function getPromotions(array $filters = []): Collection
+    {
+        $query = ShopPromotion::with('items');
+
+        // 是否只获取激活的促销活动
+        if (isset($filters['only_active']) && $filters['only_active']) {
+            $query->where('is_active', true);
+        }
+
+        // 是否只获取当前有效的促销活动
+        if (isset($filters['only_valid']) && $filters['only_valid']) {
+            $now = Carbon::now();
+            $query->where('is_active', true)
+                ->where(function ($q) use ($now) {
+                    $q->whereNull('start_time')
+                        ->orWhere('start_time', '<=', $now);
+                })
+                ->where(function ($q) use ($now) {
+                    $q->whereNull('end_time')
+                        ->orWhere('end_time', '>=', $now);
+                });
+        }
+
+        // 按分类过滤
+        if (isset($filters['category_id'])) {
+            $query->whereHas('items', function ($q) use ($filters) {
+                $q->where('category_id', $filters['category_id']);
+            });
+        }
+
+        // 按关键词搜索
+        if (isset($filters['keyword'])) {
+            $keyword = $filters['keyword'];
+            $query->where(function ($q) use ($keyword) {
+                $q->where('name', 'like', "%{$keyword}%")
+                    ->orWhere('description', 'like', "%{$keyword}%");
+            });
+        }
+
+        // 排序
+        $query->orderBy('sort_order', 'asc')
+            ->orderBy('id', 'asc');
+
+        return $query->get();
+    }
+
+    /**
+     * 获取促销活动详情
+     *
+     * @param int $promotionId 促销活动ID
+     * @return ShopPromotion|null 促销活动详情
+     */
+    public function getPromotionDetail(int $promotionId): ?ShopPromotion
+    {
+        return ShopPromotion::with('items')->find($promotionId);
+    }
+
+    /**
+     * 添加商品到促销活动
+     *
+     * @param int $promotionId 促销活动ID
+     * @param int $shopItemId 商品ID
+     * @param int|null $customDiscountValue 自定义折扣值
+     * @return ShopPromotionItem 促销商品关联
+     */
+    public function addItemToPromotion(int $promotionId, int $shopItemId, ?int $customDiscountValue = null): ShopPromotionItem
+    {
+        // 检查促销活动是否存在
+        $promotion = ShopPromotion::findOrFail($promotionId);
+
+        // 检查商品是否存在
+        $shopItem = ShopItem::findOrFail($shopItemId);
+
+        // 检查商品是否已经在促销活动中
+        $existingItem = ShopPromotionItem::where('promotion_id', $promotionId)
+            ->where('shop_item_id', $shopItemId)
+            ->first();
+
+        if ($existingItem) {
+            // 更新自定义折扣值
+            $existingItem->custom_discount_value = $customDiscountValue;
+            $existingItem->save();
+            return $existingItem;
+        }
+
+        // 创建新的关联
+        $promotionItem = new ShopPromotionItem([
+            'promotion_id' => $promotionId,
+            'shop_item_id' => $shopItemId,
+            'custom_discount_value' => $customDiscountValue,
+        ]);
+
+        $promotionItem->save();
+
+        return $promotionItem;
+    }
+
+    /**
+     * 从促销活动中移除商品
+     *
+     * @param int $promotionId 促销活动ID
+     * @param int $shopItemId 商品ID
+     * @return bool 是否成功
+     */
+    public function removeItemFromPromotion(int $promotionId, int $shopItemId): bool
+    {
+        return ShopPromotionItem::where('promotion_id', $promotionId)
+            ->where('shop_item_id', $shopItemId)
+            ->delete() > 0;
+    }
 }

+ 10 - 2
app/Module/Shop/Providers/ShopServiceProvider.php

@@ -59,10 +59,18 @@ class ShopServiceProvider extends ServiceProvider
         app('router')->group($attributes, function ($router) {
             // 商店分类路由
             $router->resource('shop/categories', \App\Module\Shop\AdminControllers\ShopCategoryController::class);
-            
+
             // 商店商品路由
             $router->resource('shop/items', \App\Module\Shop\AdminControllers\ShopItemController::class);
-            
+
+            // 商店促销活动路由
+            $router->resource('shop/promotions', \App\Module\Shop\AdminControllers\ShopPromotionController::class);
+
+            // 促销活动商品管理路由
+            $router->get('shop/promotions/{id}/items', [\App\Module\Shop\AdminControllers\ShopPromotionController::class, 'items']);
+            $router->post('shop/promotions/{id}/items/add', [\App\Module\Shop\AdminControllers\ShopPromotionController::class, 'addItem']);
+            $router->get('shop/promotions/{id}/items/remove', [\App\Module\Shop\AdminControllers\ShopPromotionController::class, 'removeItem']);
+
             // 商店购买记录路由
             $router->resource('shop/purchase-logs', \App\Module\Shop\AdminControllers\ShopPurchaseLogController::class);
         });

+ 86 - 6
app/Module/Shop/README.md

@@ -4,7 +4,7 @@ Shop模块是游戏中的商店系统,提供商品展示、购买等功能。
 
 ## 1. 模块概述
 
-Shop模块负责管理游戏中的商店系统,包括商品分类、商品信息、购买记录等。该模块与GameItems模块紧密集成,通过商店系统,玩家可以使用游戏货币购买各种物品。
+Shop模块负责管理游戏中的商店系统,包括商品分类、商品信息、促销活动、购买记录等。该模块与GameItems模块紧密集成,通过商店系统,玩家可以使用游戏货币购买各种物品,并可以享受促销折扣
 
 ## 2. 目录结构
 
@@ -14,6 +14,7 @@ app/Module/Shop/
 │   ├── Helper/              # 控制器辅助类
 │   ├── ShopCategoryController.php  # 商店分类控制器
 │   ├── ShopItemController.php      # 商店商品控制器
+│   ├── ShopPromotionController.php # 促销活动控制器
 │   └── ShopPurchaseLogController.php  # 购买记录控制器
 ├── Events/                  # 事件类
 │   └── ShopItemPurchased.php  # 商品购买事件
@@ -22,12 +23,15 @@ app/Module/Shop/
 ├── Models/                  # 数据模型
 │   ├── ShopCategory.php     # 商店分类模型
 │   ├── ShopItem.php         # 商店商品模型
+│   ├── ShopPromotion.php    # 促销活动模型
+│   ├── ShopPromotionItem.php # 促销商品关联模型
 │   └── ShopPurchaseLog.php  # 购买记录模型
 ├── Providers/               # 服务提供者
 │   └── ShopServiceProvider.php  # 商店服务提供者
 ├── Repositorys/             # 数据仓库
 │   ├── ShopCategoryRepository.php  # 商店分类数据仓库
 │   ├── ShopItemRepository.php      # 商店商品数据仓库
+│   ├── ShopPromotionRepository.php # 促销活动数据仓库
 │   └── ShopPurchaseLogRepository.php  # 购买记录数据仓库
 ├── Services/                # 服务类
 │   └── ShopService.php      # 商店服务类
@@ -40,15 +44,17 @@ app/Module/Shop/
 
 - **ShopCategory**: 商店分类模型,管理商店中的商品分类
 - **ShopItem**: 商店商品模型,管理商店中的商品信息
+- **ShopPromotion**: 促销活动模型,管理商店中的促销活动
+- **ShopPromotionItem**: 促销商品关联模型,管理促销活动与商品的关联
 - **ShopPurchaseLog**: 购买记录模型,记录用户的购买历史
 
 ### 3.2 服务
 
-- **ShopService**: 商店服务类,提供商店相关的服务方法,如获取商品列表、购买商品等
+- **ShopService**: 商店服务类,提供商店相关的服务方法,如获取商品列表、购买商品、管理促销活动
 
 ### 3.3 逻辑
 
-- **ShopLogic**: 商店逻辑类,处理商店相关的业务逻辑,如检查购买限制等
+- **ShopLogic**: 商店逻辑类,处理商店相关的业务逻辑,如检查购买限制、计算折扣价格
 
 ### 3.4 事件
 
@@ -62,17 +68,26 @@ app/Module/Shop/
 - 商品信息管理:创建、编辑、删除商品信息
 - 商品上下架:设置商品的上架时间和下架时间
 
-### 4.2 商品购买
+### 4.2 促销活动管理
+
+- 促销活动创建:创建固定折扣或百分比折扣的促销活动
+- 促销商品管理:为促销活动添加或移除商品
+- 自定义折扣:为特定商品设置自定义折扣值
+- 促销时间控制:设置促销活动的开始时间和结束时间
+
+### 4.3 商品购买
 
 - 商品购买:用户使用游戏货币购买商品
+- 折扣价格:支持促销折扣价格
 - 购买限制:设置商品的最大购买数量
 - 购买记录:记录用户的购买历史
 
-### 4.3 商店查询
+### 4.4 商店查询
 
 - 商品列表:获取商店中的商品列表
 - 分类列表:获取商店中的分类列表
-- 商品详情:获取商品的详细信息
+- 促销活动:获取当前有效的促销活动
+- 商品详情:获取商品的详细信息,包括折扣价格
 
 ## 5. 数据库表结构
 
@@ -129,6 +144,34 @@ app/Module/Shop/
 | created_at | timestamp | 创建时间 |
 | updated_at | timestamp | 更新时间 |
 
+### 5.4 shop_promotions表
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| id | int | 主键 |
+| name | varchar | 促销名称 |
+| description | text | 促销描述 |
+| banner | varchar | 促销横幅图片 |
+| discount_type | tinyint | 折扣类型(1:固定折扣, 2:百分比折扣) |
+| discount_value | int | 折扣值 |
+| is_active | tinyint | 是否激活 |
+| sort_order | int | 排序权重 |
+| start_time | timestamp | 开始时间 |
+| end_time | timestamp | 结束时间 |
+| created_at | timestamp | 创建时间 |
+| updated_at | timestamp | 更新时间 |
+
+### 5.5 shop_promotion_items表
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| id | int | 主键 |
+| promotion_id | int | 促销ID |
+| shop_item_id | int | 商品ID |
+| custom_discount_value | int | 自定义折扣值 |
+| created_at | timestamp | 创建时间 |
+| updated_at | timestamp | 更新时间 |
+
 ## 6. 使用示例
 
 ### 6.1 获取商店商品列表
@@ -141,6 +184,12 @@ $shopItems = ShopService::getShopItems();
 
 // 获取指定分类的商品
 $shopItems = ShopService::getShopItems(['category_id' => 1]);
+
+// 获取促销商品
+$shopItems = ShopService::getShopItems(['only_promotion' => true]);
+
+// 获取指定促销活动的商品
+$shopItems = ShopService::getShopItems(['promotion_id' => 1]);
 ```
 
 ### 6.2 购买商品
@@ -173,3 +222,34 @@ $purchaseLogs = ShopService::getUserPurchaseHistory($userId);
 // 获取用户指定商品的购买记录
 $purchaseLogs = ShopService::getUserPurchaseHistory($userId, ['shop_item_id' => $shopItemId]);
 ```
+
+### 6.4 获取促销活动
+
+```php
+use App\Module\Shop\Services\ShopService;
+
+// 获取所有促销活动
+$promotions = ShopService::getPromotions();
+
+// 获取当前有效的促销活动
+$promotions = ShopService::getPromotions(['only_valid' => true]);
+
+// 获取指定分类的促销活动
+$promotions = ShopService::getPromotions(['category_id' => 1]);
+```
+
+### 6.5 获取商品折扣价格
+
+```php
+use App\Module\Shop\Services\ShopService;
+
+// 获取商品价格信息
+$priceInfo = ShopService::getItemPriceInfo($shopItemId);
+
+// 价格信息包含原价、折扣价、是否有折扣、折扣百分比、促销信息等
+$originalPrice = $priceInfo['original_price'];
+$discountedPrice = $priceInfo['discounted_price'];
+$hasDiscount = $priceInfo['has_discount'];
+$discountPercentage = $priceInfo['discount_percentage'];
+$promotion = $priceInfo['promotion'];
+```

+ 104 - 23
app/Module/Shop/Services/ShopService.php

@@ -8,6 +8,8 @@ use App\Module\GameItems\Services\ItemService;
 use App\Module\Shop\Events\ShopItemPurchased;
 use App\Module\Shop\Logics\ShopLogic;
 use App\Module\Shop\Models\ShopItem;
+use App\Module\Shop\Models\ShopPromotion;
+use App\Module\Shop\Models\ShopPromotionItem;
 use App\Module\Shop\Models\ShopPurchaseLog;
 use Exception;
 use Illuminate\Database\Eloquent\Collection;
@@ -75,40 +77,40 @@ class ShopService
         try {
             // 获取商品信息
             $shopItem = ShopItem::findOrFail($shopItemId);
-            
+
             // 检查购买限制
             $shopLogic = new ShopLogic();
             list($canBuy, $errorMessage) = $shopLogic->checkBuyLimit($userId, $shopItem, $quantity);
-            
+
             if (!$canBuy) {
                 throw new LogicException($errorMessage);
             }
-            
+
             // 计算总价
             $totalPrice = $shopItem->price * $quantity;
-            
+
             // 开始事务
             DB::beginTransaction();
-            
+
             // 扣除用户货币
             $fundResult = FundUser::handle(
-                $userId, 
-                $shopItem->currency_id, 
-                -$totalPrice, 
-                LOG_TYPE::TRADE, 
-                $shopItemId, 
+                $userId,
+                $shopItem->currency_id,
+                -$totalPrice,
+                LOG_TYPE::TRADE,
+                $shopItemId,
                 "购买商品:{$shopItem->name} x {$quantity}"
             );
-            
+
             if (is_string($fundResult)) {
                 throw new LogicException("购买失败:" . $fundResult);
             }
-            
+
             // 添加物品到用户背包
             $itemResult = ItemService::addItem(
-                $userId, 
-                $shopItem->item_id, 
-                $quantity * $shopItem->item_quantity, 
+                $userId,
+                $shopItem->item_id,
+                $quantity * $shopItem->item_quantity,
                 [
                     'source_type' => 'shop_buy',
                     'source_id' => $shopItemId,
@@ -120,16 +122,16 @@ class ShopService
                     ]
                 ]
             );
-            
+
             // 记录购买记录
             $purchaseLog = $shopLogic->recordPurchase($userId, $shopItem, $quantity, $totalPrice);
-            
+
             // 触发商品购买事件
             event(new ShopItemPurchased($userId, $shopItem, $quantity, $totalPrice, $purchaseLog->id));
-            
+
             // 提交事务
             DB::commit();
-            
+
             // 返回结果
             return [
                 'success' => true,
@@ -147,13 +149,13 @@ class ShopService
                 'total_price' => $totalPrice,
                 'purchase_log_id' => $purchaseLog->id
             ];
-            
+
         } catch (Exception $e) {
             // 回滚事务
             if (DB::transactionLevel() > 0) {
                 DB::rollBack();
             }
-            
+
             Log::error('购买商品失败', [
                 'user_id' => $userId,
                 'shop_item_id' => $shopItemId,
@@ -161,7 +163,7 @@ class ShopService
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString()
             ]);
-            
+
             return false;
         }
     }
@@ -187,6 +189,85 @@ class ShopService
      */
     public static function getShopItemDetail(int $shopItemId): ?ShopItem
     {
-        return ShopItem::with(['item', 'category'])->find($shopItemId);
+        return ShopItem::with(['item', 'category', 'promotions'])->find($shopItemId);
+    }
+
+    /**
+     * 获取促销活动列表
+     *
+     * @param array $filters 过滤条件
+     * @return Collection 促销活动列表
+     */
+    public static function getPromotions(array $filters = []): Collection
+    {
+        $shopLogic = new ShopLogic();
+        return $shopLogic->getPromotions($filters);
+    }
+
+    /**
+     * 获取促销活动详情
+     *
+     * @param int $promotionId 促销活动ID
+     * @return ShopPromotion|null 促销活动详情
+     */
+    public static function getPromotionDetail(int $promotionId): ?ShopPromotion
+    {
+        $shopLogic = new ShopLogic();
+        return $shopLogic->getPromotionDetail($promotionId);
+    }
+
+    /**
+     * 添加商品到促销活动
+     *
+     * @param int $promotionId 促销活动ID
+     * @param int $shopItemId 商品ID
+     * @param int|null $customDiscountValue 自定义折扣值
+     * @return ShopPromotionItem 促销商品关联
+     */
+    public static function addItemToPromotion(int $promotionId, int $shopItemId, ?int $customDiscountValue = null): ShopPromotionItem
+    {
+        $shopLogic = new ShopLogic();
+        return $shopLogic->addItemToPromotion($promotionId, $shopItemId, $customDiscountValue);
+    }
+
+    /**
+     * 从促销活动中移除商品
+     *
+     * @param int $promotionId 促销活动ID
+     * @param int $shopItemId 商品ID
+     * @return bool 是否成功
+     */
+    public static function removeItemFromPromotion(int $promotionId, int $shopItemId): bool
+    {
+        $shopLogic = new ShopLogic();
+        return $shopLogic->removeItemFromPromotion($promotionId, $shopItemId);
+    }
+
+    /**
+     * 获取商品的折扣价格
+     *
+     * @param int $shopItemId 商品ID
+     * @return array 价格信息
+     */
+    public static function getItemPriceInfo(int $shopItemId): array
+    {
+        $shopItem = ShopItem::findOrFail($shopItemId);
+        $originalPrice = $shopItem->price;
+        $discountedPrice = $shopItem->getDiscountedPrice();
+        $promotion = $shopItem->getActivePromotion();
+
+        return [
+            'original_price' => $originalPrice,
+            'discounted_price' => $discountedPrice,
+            'has_discount' => $discountedPrice < $originalPrice,
+            'discount_percentage' => $originalPrice > 0 ? round((1 - $discountedPrice / $originalPrice) * 100) : 0,
+            'promotion' => $promotion ? [
+                'id' => $promotion->id,
+                'name' => $promotion->name,
+                'discount_type' => $promotion->discount_type,
+                'discount_value' => $promotion->discount_value,
+                'end_time' => $promotion->end_time ? $promotion->end_time->toDateTimeString() : null
+            ] : null
+        ];
     }
 }

+ 45 - 0
resources/views/admin/shop/promotion_info.blade.php

@@ -0,0 +1,45 @@
+<div class="table-responsive">
+    <table class="table table-bordered">
+        <tbody>
+            <tr>
+                <th style="width: 150px;">活动名称</th>
+                <td>{{ $promotion->name }}</td>
+                <th style="width: 150px;">折扣类型</th>
+                <td>
+                    @if($promotion->discount_type == \App\Module\Shop\Models\ShopPromotion::DISCOUNT_TYPE_FIXED)
+                        固定折扣
+                    @else
+                        百分比折扣
+                    @endif
+                </td>
+            </tr>
+            <tr>
+                <th>折扣值</th>
+                <td>
+                    @if($promotion->discount_type == \App\Module\Shop\Models\ShopPromotion::DISCOUNT_TYPE_FIXED)
+                        {{ $promotion->discount_value }} 元
+                    @else
+                        {{ $promotion->discount_value }}%
+                    @endif
+                </td>
+                <th>状态</th>
+                <td>{{ $promotion->is_active ? '启用' : '禁用' }}</td>
+            </tr>
+            <tr>
+                <th>开始时间</th>
+                <td>{{ $promotion->start_time ? $promotion->start_time->format('Y-m-d H:i:s') : '不限制' }}</td>
+                <th>结束时间</th>
+                <td>{{ $promotion->end_time ? $promotion->end_time->format('Y-m-d H:i:s') : '不限制' }}</td>
+            </tr>
+            <tr>
+                <th>活动描述</th>
+                <td colspan="3">{{ $promotion->description }}</td>
+            </tr>
+        </tbody>
+    </table>
+</div>
+
+<div class="mt-2">
+    <a href="{{ admin_url('shop/promotions/' . $promotion->id . '/edit') }}" class="btn btn-primary">编辑活动</a>
+    <a href="{{ admin_url('shop/promotions') }}" class="btn btn-default">返回列表</a>
+</div>