diff --git a/app/Admin/Controllers/CarmisController.php b/app/Admin/Controllers/CarmisController.php
index 18ccbcbf2..f6aaba329 100755
--- a/app/Admin/Controllers/CarmisController.php
+++ b/app/Admin/Controllers/CarmisController.php
@@ -26,10 +26,13 @@ class CarmisController extends AdminController
*/
protected function grid()
{
- return Grid::make(new Carmis(['goods']), function (Grid $grid) {
+ return Grid::make(new Carmis(['goods', 'goodsSku']), function (Grid $grid) {
$grid->model()->orderBy('id', 'DESC');
$grid->column('id')->sortable();
$grid->column('goods.gd_name', admin_trans('carmis.fields.goods_id'));
+ $grid->column('goodsSku.name', '商品规格')->display(function ($name) {
+ return $name ?: '-';
+ });
$grid->column('status')->select(CarmisModel::getStatusMap());
$grid->column('is_loop')->display(function($v){return $v==1?admin_trans('carmis.fields.yes'):"";});
$grid->column('carmi')->limit(20);
@@ -40,6 +43,9 @@ protected function grid()
$filter->equal('goods_id')->select(
Goods::query()->where('type', Goods::AUTOMATIC_DELIVERY)->pluck('gd_name', 'id')
);
+ $filter->equal('goods_sku_id', '商品规格')->select(function () {
+ return \App\Models\GoodsSku::with('goods')->get()->pluck('name_with_goods', 'id');
+ });
$filter->equal('status')->select(CarmisModel::getStatusMap());
$filter->scope(admin_trans('dujiaoka.trashed'))->onlyTrashed();
});
@@ -94,7 +100,11 @@ protected function form()
$form->display('id');
$form->select('goods_id')->options(
Goods::query()->where('type', Goods::AUTOMATIC_DELIVERY)->pluck('gd_name', 'id')
- )->required();
+ )->required()->load('goods_sku_id', '/admin/api/goods-skus');
+
+ $form->select('goods_sku_id', '商品规格')
+ ->help('可选,如果商品有多规格请选择对应规格');
+
$form->radio('status')
->options(CarmisModel::getStatusMap())
->default(CarmisModel::STATUS_UNSOLD);
diff --git a/app/Admin/Controllers/GoodsController.php b/app/Admin/Controllers/GoodsController.php
index 72a637b51..31cae2aeb 100755
--- a/app/Admin/Controllers/GoodsController.php
+++ b/app/Admin/Controllers/GoodsController.php
@@ -45,11 +45,20 @@ protected function grid()
$grid->column('in_stock')->display(function () {
// 如果为自动发货,则加载库存卡密
if ($this->type == GoodsModel::AUTOMATIC_DELIVERY) {
- return Carmis::query()->where('goods_id', $this->id)
- ->where('status', Carmis::STATUS_UNSOLD)
- ->count();
+ if ($this->has_sku) {
+ // 有多规格的商品,统计所有SKU的卡密
+ return Carmis::query()->whereIn('goods_sku_id', $this->skus->pluck('id'))
+ ->where('status', Carmis::STATUS_UNSOLD)
+ ->count();
+ } else {
+ // 没有多规格的商品
+ return Carmis::query()->where('goods_id', $this->id)
+ ->whereNull('goods_sku_id')
+ ->where('status', Carmis::STATUS_UNSOLD)
+ ->count();
+ }
} else {
- return $this->in_stock;
+ return $this->has_sku ? $this->skus->sum('stock') : $this->in_stock;
}
});
$grid->column('sales_volume');
@@ -70,6 +79,11 @@ protected function grid()
$grid->actions(function (Grid\Displayers\Actions $actions) {
if (request('_scope_') == admin_trans('dujiaoka.trashed')) {
$actions->append(new Restore(GoodsModel::class));
+ } else {
+ // 添加规格管理按钮
+ $actions->append('规格管理');
+ // 添加卡密管理按钮
+ $actions->append('卡密管理');
}
});
$grid->batchActions(function (Grid\Tools\BatchActions $batch) {
@@ -143,9 +157,13 @@ protected function form()
)->required();
$form->image('picture')->autoUpload()->uniqueName()->help(admin_trans('goods.helps.picture'));
$form->radio('type')->options(GoodsModel::getGoodsTypeMap())->default(GoodsModel::AUTOMATIC_DELIVERY)->required();
+
+ // 多规格设置
+ $form->switch('has_sku', '启用多规格')->default(0)->help('启用后可以为商品设置多个规格');
+
$form->currency('retail_price')->default(0)->help(admin_trans('goods.helps.retail_price'));
- $form->currency('actual_price')->default(0)->required();
- $form->number('in_stock')->help(admin_trans('goods.helps.in_stock'));
+ $form->currency('actual_price')->default(0)->required()->help('单规格商品价格,多规格商品此价格作为基础价格');
+ $form->number('in_stock')->help('单规格商品库存,多规格商品请在规格管理中设置');
$form->number('sales_volume');
$form->number('buy_limit_num')->help(admin_trans('goods.helps.buy_limit_num'));
$form->editor('buy_prompt');
diff --git a/app/Admin/Controllers/GoodsSkuController.php b/app/Admin/Controllers/GoodsSkuController.php
new file mode 100644
index 000000000..176311749
--- /dev/null
+++ b/app/Admin/Controllers/GoodsSkuController.php
@@ -0,0 +1,174 @@
+column('id', 'ID')->sortable();
+ $grid->column('goods.gd_name', '商品名称');
+ $grid->column('sku_code', 'SKU编码');
+ $grid->column('name', '规格名称');
+ $grid->column('attributes', '规格属性')->display(function ($attributes) {
+ if (!$attributes) return '-';
+ $attrs = [];
+ foreach ($attributes as $key => $value) {
+ $attrs[] = $key . ': ' . $value;
+ }
+ return implode(', ', $attrs);
+ });
+ $grid->column('price', '价格')->display(function ($price) {
+ return '¥' . number_format($price, 2);
+ });
+ $grid->column('stock', '库存');
+ $grid->column('sold_count', '销量');
+ $grid->column('status', '状态')->using([
+ GoodsSkuModel::STATUS_DISABLED => '禁用',
+ GoodsSkuModel::STATUS_ENABLED => '启用'
+ ])->label([
+ GoodsSkuModel::STATUS_DISABLED => 'danger',
+ GoodsSkuModel::STATUS_ENABLED => 'success'
+ ]);
+ $grid->column('sort', '排序');
+ $grid->column('created_at', '创建时间');
+
+ $grid->filter(function (Grid\Filter $filter) {
+ $filter->like('sku_code', 'SKU编码');
+ $filter->like('name', '规格名称');
+ $filter->equal('goods_id', '商品')->select(function () {
+ return Goods::pluck('gd_name', 'id');
+ });
+ $filter->equal('status', '状态')->select([
+ GoodsSkuModel::STATUS_DISABLED => '禁用',
+ GoodsSkuModel::STATUS_ENABLED => '启用'
+ ]);
+ });
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+ $actions->append('管理卡密');
+ });
+ });
+ }
+
+ /**
+ * 详情页面
+ */
+ protected function detail($id)
+ {
+ return Show::make($id, new GoodsSku(), function (Show $show) {
+ $show->field('id', 'ID');
+ $show->field('goods.gd_name', '商品名称');
+ $show->field('sku_code', 'SKU编码');
+ $show->field('name', '规格名称');
+ $show->field('attributes', '规格属性')->json();
+ $show->field('price', '价格')->as(function ($price) {
+ return '¥' . number_format($price, 2);
+ });
+ $show->field('wholesale_price', '批发价格')->as(function ($price) {
+ return $price ? '¥' . number_format($price, 2) : '-';
+ });
+ $show->field('cost_price', '成本价格')->as(function ($price) {
+ return $price ? '¥' . number_format($price, 2) : '-';
+ });
+ $show->field('stock', '库存');
+ $show->field('sold_count', '销量');
+ $show->field('warning_stock', '预警库存');
+ $show->field('status', '状态')->using([
+ GoodsSkuModel::STATUS_DISABLED => '禁用',
+ GoodsSkuModel::STATUS_ENABLED => '启用'
+ ]);
+ $show->field('image', '规格图片')->image();
+ $show->field('weight', '重量(kg)');
+ $show->field('barcode', '条形码');
+ $show->field('supplier_code', '供应商编码');
+ $show->field('sort', '排序');
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '更新时间');
+ });
+ }
+
+ /**
+ * 表单页面
+ */
+ protected function form()
+ {
+ return Form::make(new GoodsSku(), function (Form $form) {
+ $form->display('id', 'ID');
+
+ $form->select('goods_id', '商品')
+ ->options(function () {
+ return Goods::pluck('gd_name', 'id');
+ })
+ ->required()
+ ->help('选择要添加规格的商品');
+
+ $form->text('sku_code', 'SKU编码')
+ ->required()
+ ->help('唯一的SKU编码,留空将自动生成');
+
+ $form->text('name', '规格名称')->required();
+
+ $form->keyValue('attributes', '规格属性')
+ ->help('设置规格的属性,如颜色、尺寸等');
+
+ $form->currency('price', '价格')->symbol('¥')->required();
+ $form->currency('wholesale_price', '批发价格')->symbol('¥');
+ $form->currency('cost_price', '成本价格')->symbol('¥');
+
+ $form->number('stock', '库存')->default(0)->min(0);
+ $form->number('warning_stock', '预警库存')->default(10)->min(0);
+
+ $form->radio('status', '状态')->options([
+ GoodsSkuModel::STATUS_DISABLED => '禁用',
+ GoodsSkuModel::STATUS_ENABLED => '启用'
+ ])->default(GoodsSkuModel::STATUS_ENABLED)->required();
+
+ $form->image('image', '规格图片')->uniqueName();
+ $form->decimal('weight', '重量(kg)')->help('商品重量,单位:千克');
+ $form->text('barcode', '条形码');
+ $form->text('supplier_code', '供应商编码');
+ $form->number('sort', '排序')->default(0)->help('数值越大排序越靠前');
+
+ $form->display('sold_count', '销量');
+ $form->display('created_at', '创建时间');
+ $form->display('updated_at', '更新时间');
+
+ // 保存前处理
+ $form->saving(function (Form $form) {
+ // 如果没有填写SKU编码,自动生成
+ if (empty($form->sku_code)) {
+ $form->sku_code = GoodsSkuModel::generateSkuCode($form->goods_id);
+ }
+ });
+
+ // 保存后处理
+ $form->saved(function (Form $form) {
+ // 更新商品的价格范围
+ $goods = Goods::find($form->model()->goods_id);
+ if ($goods) {
+ $goods->update(['has_sku' => 1]);
+ $goods->updatePriceRange();
+ }
+ });
+ });
+ }
+}
diff --git a/app/Admin/Controllers/ShoppingCartController.php b/app/Admin/Controllers/ShoppingCartController.php
new file mode 100644
index 000000000..9a8a27cf5
--- /dev/null
+++ b/app/Admin/Controllers/ShoppingCartController.php
@@ -0,0 +1,155 @@
+column('id', 'ID')->sortable();
+ $grid->column('session_id', '会话ID')->limit(20);
+ $grid->column('user_email', '用户邮箱');
+ $grid->column('goods.gd_name', '商品名称');
+ $grid->column('goodsSku.name', '规格名称');
+ $grid->column('quantity', '数量');
+ $grid->column('price', '单价')->display(function ($price) {
+ return '¥' . number_format($price, 2);
+ });
+ $grid->column('total_price', '总价')->display(function ($price) {
+ return '¥' . number_format($price, 2);
+ });
+ $grid->column('discount_amount', '优惠金额')->display(function ($amount) {
+ return $amount > 0 ? '¥' . number_format($amount, 2) : '-';
+ });
+ $grid->column('coupon_code', '优惠券');
+ $grid->column('expires_at', '过期时间');
+ $grid->column('created_at', '创建时间');
+
+ $grid->filter(function (Grid\Filter $filter) {
+ $filter->like('session_id', '会话ID');
+ $filter->like('user_email', '用户邮箱');
+ $filter->like('goods.gd_name', '商品名称');
+ $filter->like('coupon_code', '优惠券代码');
+ $filter->between('created_at', '创建时间')->datetime();
+ });
+
+ // 禁用新增和编辑
+ $grid->disableCreateButton();
+ $grid->disableEditButton();
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+ $actions->disableEdit();
+ });
+
+ // 批量操作
+ $grid->batchActions(function (Grid\Tools\BatchActions $batch) {
+ $batch->add('清理过期', new \App\Admin\Actions\ClearExpiredCarts());
+ });
+
+ // 工具栏
+ $grid->tools(function (Grid\Tools $tools) {
+ $tools->append('统计数据');
+ });
+ });
+ }
+
+ /**
+ * 详情页面
+ */
+ protected function detail($id)
+ {
+ return Show::make($id, new ShoppingCart(), function (Show $show) {
+ $show->field('id', 'ID');
+ $show->field('session_id', '会话ID');
+ $show->field('user_email', '用户邮箱');
+ $show->field('goods.gd_name', '商品名称');
+ $show->field('goodsSku.name', '规格名称');
+ $show->field('quantity', '数量');
+ $show->field('price', '单价')->as(function ($price) {
+ return '¥' . number_format($price, 2);
+ });
+ $show->field('total_price', '总价')->as(function ($price) {
+ return '¥' . number_format($price, 2);
+ });
+ $show->field('discount_amount', '优惠金额')->as(function ($amount) {
+ return $amount > 0 ? '¥' . number_format($amount, 2) : '-';
+ });
+ $show->field('coupon_code', '优惠券代码');
+ $show->field('goods_snapshot', '商品快照')->json();
+ $show->field('sku_snapshot', '规格快照')->json();
+ $show->field('custom_fields', '自定义字段')->json();
+ $show->field('expires_at', '过期时间');
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '更新时间');
+ });
+ }
+
+ /**
+ * 购物车统计
+ */
+ public function statistics()
+ {
+ $stats = [
+ 'total_carts' => ShoppingCartModel::count(),
+ 'active_carts' => ShoppingCartModel::notExpired()->count(),
+ 'expired_carts' => ShoppingCartModel::expired()->count(),
+ 'total_value' => ShoppingCartModel::notExpired()->sum('total_price'),
+ 'avg_cart_value' => ShoppingCartModel::notExpired()->avg('total_price'),
+ 'today_carts' => ShoppingCartModel::whereDate('created_at', today())->count(),
+ 'week_carts' => ShoppingCartModel::whereBetween('created_at', [
+ now()->startOfWeek(),
+ now()->endOfWeek()
+ ])->count(),
+ 'month_carts' => ShoppingCartModel::whereMonth('created_at', now()->month)->count(),
+ ];
+
+ // 热门商品统计
+ $popularGoods = ShoppingCartModel::notExpired()
+ ->selectRaw('goods_id, COUNT(*) as cart_count, SUM(quantity) as total_quantity')
+ ->groupBy('goods_id')
+ ->orderBy('cart_count', 'desc')
+ ->limit(10)
+ ->with('goods')
+ ->get();
+
+ // 用户购物车统计
+ $userStats = ShoppingCartModel::notExpired()
+ ->whereNotNull('user_email')
+ ->selectRaw('user_email, COUNT(*) as cart_count, SUM(total_price) as total_value')
+ ->groupBy('user_email')
+ ->orderBy('total_value', 'desc')
+ ->limit(10)
+ ->get();
+
+ return view('admin.shopping-cart.statistics', compact('stats', 'popularGoods', 'userStats'));
+ }
+
+ /**
+ * 清理过期购物车
+ */
+ public function clearExpired()
+ {
+ $count = ShoppingCartModel::clearExpired();
+
+ return response()->json([
+ 'status' => true,
+ 'message' => "成功清理 {$count} 个过期购物车项"
+ ]);
+ }
+}
diff --git a/app/Admin/Controllers/SubsiteController.php b/app/Admin/Controllers/SubsiteController.php
new file mode 100644
index 000000000..7dd347ee3
--- /dev/null
+++ b/app/Admin/Controllers/SubsiteController.php
@@ -0,0 +1,212 @@
+column('id', 'ID')->sortable();
+ $grid->column('name', '分站名称');
+ $grid->column('domain', '分站域名');
+ $grid->column('type', '分站类型')->using([
+ SubsiteModel::TYPE_LOCAL => '本站分站',
+ SubsiteModel::TYPE_THIRD_PARTY => '第三方对接'
+ ])->label([
+ SubsiteModel::TYPE_LOCAL => 'primary',
+ SubsiteModel::TYPE_THIRD_PARTY => 'success'
+ ]);
+ $grid->column('status', '状态')->using([
+ SubsiteModel::STATUS_DISABLED => '禁用',
+ SubsiteModel::STATUS_ENABLED => '启用'
+ ])->label([
+ SubsiteModel::STATUS_DISABLED => 'danger',
+ SubsiteModel::STATUS_ENABLED => 'success'
+ ]);
+ $grid->column('commission_rate', '佣金比例(%)');
+ $grid->column('balance', '余额')->display(function ($balance) {
+ return '¥' . number_format($balance, 2);
+ });
+ $grid->column('last_sync_at', '最后同步时间');
+ $grid->column('created_at', '创建时间');
+
+ $grid->filter(function (Grid\Filter $filter) {
+ $filter->equal('name', '分站名称');
+ $filter->equal('type', '分站类型')->select([
+ SubsiteModel::TYPE_LOCAL => '本站分站',
+ SubsiteModel::TYPE_THIRD_PARTY => '第三方对接'
+ ]);
+ $filter->equal('status', '状态')->select([
+ SubsiteModel::STATUS_DISABLED => '禁用',
+ SubsiteModel::STATUS_ENABLED => '启用'
+ ]);
+ });
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+ $actions->append('订单管理');
+ $actions->append('统计数据');
+ });
+ });
+ }
+
+ /**
+ * 详情页面
+ */
+ protected function detail($id)
+ {
+ return Show::make($id, new Subsite(), function (Show $show) {
+ $show->field('id', 'ID');
+ $show->field('name', '分站名称');
+ $show->field('domain', '分站域名');
+ $show->field('subdomain', '子域名');
+ $show->field('type', '分站类型')->using([
+ SubsiteModel::TYPE_LOCAL => '本站分站',
+ SubsiteModel::TYPE_THIRD_PARTY => '第三方对接'
+ ]);
+ $show->field('status', '状态')->using([
+ SubsiteModel::STATUS_DISABLED => '禁用',
+ SubsiteModel::STATUS_ENABLED => '启用'
+ ]);
+ $show->field('commission_rate', '佣金比例(%)');
+ $show->field('balance', '余额')->as(function ($balance) {
+ return '¥' . number_format($balance, 2);
+ });
+ $show->field('api_url', 'API地址');
+ $show->field('api_key', 'API密钥');
+ $show->field('contact_email', '联系邮箱');
+ $show->field('contact_phone', '联系电话');
+ $show->field('description', '分站描述');
+ $show->field('last_sync_at', '最后同步时间');
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '更新时间');
+ });
+ }
+
+ /**
+ * 表单页面
+ */
+ protected function form()
+ {
+ return Form::make(new Subsite(), function (Form $form) {
+ $form->display('id', 'ID');
+ $form->text('name', '分站名称')->required();
+ $form->text('domain', '分站域名')->required()->help('请输入完整的域名,如:shop.example.com');
+ $form->text('subdomain', '子域名')->help('可选,用于生成子域名访问');
+
+ $form->radio('type', '分站类型')->options([
+ SubsiteModel::TYPE_LOCAL => '本站分站',
+ SubsiteModel::TYPE_THIRD_PARTY => '第三方对接'
+ ])->default(SubsiteModel::TYPE_LOCAL)->required();
+
+ $form->radio('status', '状态')->options([
+ SubsiteModel::STATUS_DISABLED => '禁用',
+ SubsiteModel::STATUS_ENABLED => '启用'
+ ])->default(SubsiteModel::STATUS_ENABLED)->required();
+
+ $form->decimal('commission_rate', '佣金比例(%)')
+ ->default(0)
+ ->min(0)
+ ->max(100)
+ ->help('设置分站的佣金比例,0-100之间');
+
+ $form->divider('API配置');
+ $form->url('api_url', 'API地址')->help('第三方对接时需要填写');
+ $form->text('api_key', 'API密钥')->help('API访问密钥');
+ $form->password('api_secret', 'API秘钥')->help('API访问秘钥');
+
+ $form->divider('联系信息');
+ $form->email('contact_email', '联系邮箱');
+ $form->text('contact_phone', '联系电话');
+ $form->textarea('description', '分站描述');
+
+ $form->display('balance', '当前余额')->with(function ($value) {
+ return '¥' . number_format($value ?? 0, 2);
+ });
+
+ $form->display('created_at', '创建时间');
+ $form->display('updated_at', '更新时间');
+ });
+ }
+
+ /**
+ * 分站订单管理
+ */
+ public function orders($id)
+ {
+ $subsite = SubsiteModel::findOrFail($id);
+
+ return Grid::make(new \App\Admin\Repositories\SubsiteOrder($id), function (Grid $grid) use ($subsite) {
+ $grid->model()->where('subsite_id', $subsite->id);
+
+ $grid->column('id', 'ID')->sortable();
+ $grid->column('order.order_sn', '订单号');
+ $grid->column('order.goods.gd_name', '商品名称');
+ $grid->column('commission_amount', '佣金金额')->display(function ($amount) {
+ return '¥' . number_format($amount, 2);
+ });
+ $grid->column('commission_status', '佣金状态')->using([
+ 0 => '未结算',
+ 1 => '已结算'
+ ])->label([
+ 0 => 'warning',
+ 1 => 'success'
+ ]);
+ $grid->column('sync_status', '同步状态')->using([
+ 0 => '未同步',
+ 1 => '已同步',
+ 2 => '同步失败'
+ ])->label([
+ 0 => 'default',
+ 1 => 'success',
+ 2 => 'danger'
+ ]);
+ $grid->column('created_at', '创建时间');
+
+ $grid->filter(function (Grid\Filter $filter) {
+ $filter->like('order.order_sn', '订单号');
+ $filter->equal('commission_status', '佣金状态')->select([
+ 0 => '未结算',
+ 1 => '已结算'
+ ]);
+ $filter->equal('sync_status', '同步状态')->select([
+ 0 => '未同步',
+ 1 => '已同步',
+ 2 => '同步失败'
+ ]);
+ });
+
+ $grid->header(function () use ($subsite) {
+ return '
分站:' . $subsite->name . ' - 订单管理
';
+ });
+ });
+ }
+
+ /**
+ * 分站统计
+ */
+ public function statistics($id)
+ {
+ $subsite = SubsiteModel::findOrFail($id);
+ $subsiteService = app('\App\Services\SubsiteService');
+ $stats = $subsiteService->getSubsiteStatistics($subsite);
+
+ return view('admin.subsite.statistics', compact('subsite', 'stats'));
+ }
+}
diff --git a/app/Admin/Repositories/GoodsSku.php b/app/Admin/Repositories/GoodsSku.php
new file mode 100644
index 000000000..4e8db4513
--- /dev/null
+++ b/app/Admin/Repositories/GoodsSku.php
@@ -0,0 +1,21 @@
+subsiteId = $subsiteId;
+ parent::__construct();
+ }
+
+ public function with($relations)
+ {
+ $this->relations = array_merge($this->relations, (array) $relations);
+ return $this;
+ }
+}
diff --git a/app/Admin/routes.php b/app/Admin/routes.php
index 8f1181153..b32ab9fbb 100644
--- a/app/Admin/routes.php
+++ b/app/Admin/routes.php
@@ -19,6 +19,31 @@
$router->resource('emailtpl', 'EmailtplController');
$router->resource('pay', 'PayController');
$router->resource('order', 'OrderController');
+
+ // 分站管理
+ $router->resource('subsite', 'SubsiteController');
+ $router->get('subsite/{id}/orders', 'SubsiteController@orders');
+ $router->get('subsite/{id}/statistics', 'SubsiteController@statistics');
+
+ // 商品规格管理
+ $router->resource('goods-sku', 'GoodsSkuController');
+
+ // 购物车管理
+ $router->resource('shopping-cart', 'ShoppingCartController');
+ $router->get('shopping-cart/statistics', 'ShoppingCartController@statistics');
+ $router->post('shopping-cart/clear-expired', 'ShoppingCartController@clearExpired');
+
+ // API路由
+ $router->get('api/goods-skus', function () {
+ $goodsId = request('q');
+ if (!$goodsId) {
+ return [];
+ }
+ return \App\Models\GoodsSku::where('goods_id', $goodsId)
+ ->where('status', \App\Models\GoodsSku::STATUS_ENABLED)
+ ->pluck('name', 'id');
+ });
+
$router->get('import-carmis', 'CarmisController@importCarmis');
$router->get('system-setting', 'SystemSettingController@systemSetting');
$router->get('email-test', 'EmailTestController@emailTest');
diff --git a/app/Http/Controllers/Admin/SubsiteController.php b/app/Http/Controllers/Admin/SubsiteController.php
new file mode 100644
index 000000000..49bb4bf00
--- /dev/null
+++ b/app/Http/Controllers/Admin/SubsiteController.php
@@ -0,0 +1,289 @@
+subsiteService = $subsiteService;
+ }
+
+ /**
+ * 分站列表
+ */
+ public function index(Request $request)
+ {
+ $query = Subsite::query();
+
+ // 搜索条件
+ if ($request->filled('name')) {
+ $query->where('name', 'like', '%' . $request->name . '%');
+ }
+
+ if ($request->filled('type')) {
+ $query->where('type', $request->type);
+ }
+
+ if ($request->filled('status')) {
+ $query->where('status', $request->status);
+ }
+
+ $subsites = $query->orderBy('created_at', 'desc')->paginate(20);
+
+ return view('admin.subsite.index', compact('subsites'));
+ }
+
+ /**
+ * 创建分站页面
+ */
+ public function create()
+ {
+ return view('admin.subsite.create');
+ }
+
+ /**
+ * 保存分站
+ */
+ public function store(Request $request): JsonResponse
+ {
+ $validated = $request->validate([
+ 'name' => 'required|string|max:255',
+ 'domain' => 'required|string|unique:subsites,domain',
+ 'subdomain' => 'nullable|string|unique:subsites,subdomain',
+ 'type' => 'required|integer|in:1,2',
+ 'commission_rate' => 'required|numeric|min:0|max:100',
+ 'api_url' => 'nullable|url',
+ 'api_key' => 'nullable|string',
+ 'api_secret' => 'nullable|string',
+ 'contact_email' => 'nullable|email',
+ 'contact_phone' => 'nullable|string',
+ 'description' => 'nullable|string'
+ ]);
+
+ try {
+ $subsite = $this->subsiteService->createSubsite($validated);
+ return response()->json([
+ 'code' => 200,
+ 'message' => '分站创建成功',
+ 'data' => $subsite
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '创建失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 编辑分站页面
+ */
+ public function edit(Subsite $subsite)
+ {
+ return view('admin.subsite.edit', compact('subsite'));
+ }
+
+ /**
+ * 更新分站
+ */
+ public function update(Request $request, Subsite $subsite): JsonResponse
+ {
+ $validated = $request->validate([
+ 'name' => 'required|string|max:255',
+ 'domain' => 'required|string|unique:subsites,domain,' . $subsite->id,
+ 'subdomain' => 'nullable|string|unique:subsites,subdomain,' . $subsite->id,
+ 'type' => 'required|integer|in:1,2',
+ 'commission_rate' => 'required|numeric|min:0|max:100',
+ 'api_url' => 'nullable|url',
+ 'api_key' => 'nullable|string',
+ 'api_secret' => 'nullable|string',
+ 'contact_email' => 'nullable|email',
+ 'contact_phone' => 'nullable|string',
+ 'description' => 'nullable|string'
+ ]);
+
+ try {
+ $this->subsiteService->updateSubsite($subsite, $validated);
+ return response()->json([
+ 'code' => 200,
+ 'message' => '分站更新成功'
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '更新失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 删除分站
+ */
+ public function destroy(Subsite $subsite): JsonResponse
+ {
+ try {
+ $this->subsiteService->deleteSubsite($subsite);
+ return response()->json([
+ 'code' => 200,
+ 'message' => '分站删除成功'
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '删除失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 切换分站状态
+ */
+ public function toggleStatus(Subsite $subsite): JsonResponse
+ {
+ try {
+ $newStatus = $subsite->status === Subsite::STATUS_ENABLED
+ ? Subsite::STATUS_DISABLED
+ : Subsite::STATUS_ENABLED;
+
+ $subsite->update(['status' => $newStatus]);
+
+ return response()->json([
+ 'code' => 200,
+ 'message' => '状态更新成功',
+ 'status' => $newStatus
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '状态更新失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 分站订单列表
+ */
+ public function orders(Subsite $subsite, Request $request)
+ {
+ $query = $subsite->orders()->with(['order', 'order.goods']);
+
+ // 搜索条件
+ if ($request->filled('order_sn')) {
+ $query->whereHas('order', function($q) use ($request) {
+ $q->where('order_sn', 'like', '%' . $request->order_sn . '%');
+ });
+ }
+
+ if ($request->filled('commission_status')) {
+ $query->where('commission_status', $request->commission_status);
+ }
+
+ if ($request->filled('sync_status')) {
+ $query->where('sync_status', $request->sync_status);
+ }
+
+ $orders = $query->orderBy('created_at', 'desc')->paginate(20);
+
+ return view('admin.subsite.orders', compact('subsite', 'orders'));
+ }
+
+ /**
+ * 佣金结算
+ */
+ public function settleCommission(Request $request): JsonResponse
+ {
+ $validated = $request->validate([
+ 'order_ids' => 'required|array',
+ 'order_ids.*' => 'exists:subsite_orders,id'
+ ]);
+
+ try {
+ $result = $this->subsiteService->settleCommissions($validated['order_ids']);
+ return response()->json([
+ 'code' => 200,
+ 'message' => '佣金结算成功',
+ 'data' => $result
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '结算失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 同步订单到分站
+ */
+ public function syncOrders(Subsite $subsite): JsonResponse
+ {
+ try {
+ $result = $this->subsiteService->syncOrdersToSubsite($subsite);
+ return response()->json([
+ 'code' => 200,
+ 'message' => '同步成功',
+ 'data' => $result
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '同步失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 测试API连接
+ */
+ public function testApi(Subsite $subsite): JsonResponse
+ {
+ try {
+ $result = $this->subsiteService->testApiConnection($subsite);
+ return response()->json([
+ 'code' => 200,
+ 'message' => 'API连接测试成功',
+ 'data' => $result
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => 'API连接测试失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 分站统计数据
+ */
+ public function statistics(Subsite $subsite): JsonResponse
+ {
+ try {
+ $stats = $this->subsiteService->getSubsiteStatistics($subsite);
+ return response()->json([
+ 'code' => 200,
+ 'data' => $stats
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '获取统计数据失败:' . $e->getMessage()
+ ]);
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/SubsiteController.php b/app/Http/Controllers/Api/SubsiteController.php
new file mode 100644
index 000000000..6f27201de
--- /dev/null
+++ b/app/Http/Controllers/Api/SubsiteController.php
@@ -0,0 +1,283 @@
+json([
+ 'status' => 'success',
+ 'message' => 'API连接正常',
+ 'timestamp' => now()->toISOString(),
+ 'version' => '1.0.0'
+ ]);
+ }
+
+ /**
+ * 同步订单接口
+ */
+ public function syncOrder(Request $request): JsonResponse
+ {
+ try {
+ // 验证API密钥
+ $apiKey = $request->header('Authorization');
+ $apiSecret = $request->header('X-API-Secret');
+
+ if (!$apiKey || !$apiSecret) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => 'API密钥缺失'
+ ], 401);
+ }
+
+ // 去掉Bearer前缀
+ $apiKey = str_replace('Bearer ', '', $apiKey);
+
+ // 验证分站
+ $subsite = Subsite::where('api_key', $apiKey)
+ ->where('api_secret', $apiSecret)
+ ->where('status', Subsite::STATUS_ENABLED)
+ ->first();
+
+ if (!$subsite) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => 'API密钥无效'
+ ], 401);
+ }
+
+ // 验证请求数据
+ $validated = $request->validate([
+ 'order_sn' => 'required|string',
+ 'goods_name' => 'required|string',
+ 'goods_price' => 'required|numeric',
+ 'quantity' => 'required|integer|min:1',
+ 'total_price' => 'required|numeric',
+ 'email' => 'required|email',
+ 'contact' => 'nullable|string',
+ 'status' => 'required|integer',
+ 'sku_code' => 'nullable|string',
+ 'sku_name' => 'nullable|string',
+ 'sku_attributes' => 'nullable|array'
+ ]);
+
+ // 检查订单是否已存在
+ $existingOrder = Order::where('order_sn', $validated['order_sn'])->first();
+ if ($existingOrder) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => '订单号已存在',
+ 'order_sn' => $validated['order_sn']
+ ], 409);
+ }
+
+ // 创建订单记录(这里可以根据实际需求调整)
+ $orderData = [
+ 'order_sn' => $validated['order_sn'],
+ 'email' => $validated['email'],
+ 'contact' => $validated['contact'] ?? '',
+ 'buy_amount' => $validated['quantity'],
+ 'actual_price' => $validated['goods_price'],
+ 'total_price' => $validated['total_price'],
+ 'status' => $validated['status'],
+ 'info' => json_encode([
+ 'gd_name' => $validated['goods_name'],
+ 'source' => 'subsite_sync',
+ 'subsite_id' => $subsite->id
+ ])
+ ];
+
+ // 如果有SKU信息
+ if ($validated['sku_code']) {
+ $orderData['sku_snapshot'] = json_encode([
+ 'sku_code' => $validated['sku_code'],
+ 'name' => $validated['sku_name'],
+ 'attributes' => $validated['sku_attributes']
+ ]);
+ }
+
+ // 这里可以根据实际需求决定是否真的创建订单
+ // 或者只是记录同步信息
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => '订单同步成功',
+ 'order_sn' => $validated['order_sn'],
+ 'sync_time' => now()->toISOString()
+ ]);
+
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => '数据验证失败',
+ 'errors' => $e->errors()
+ ], 422);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => '同步失败:' . $e->getMessage()
+ ], 500);
+ }
+ }
+
+ /**
+ * 获取商品列表
+ */
+ public function getGoods(Request $request): JsonResponse
+ {
+ try {
+ // 验证API密钥
+ $apiKey = $request->header('Authorization');
+ $apiSecret = $request->header('X-API-Secret');
+
+ if (!$apiKey || !$apiSecret) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => 'API密钥缺失'
+ ], 401);
+ }
+
+ $apiKey = str_replace('Bearer ', '', $apiKey);
+
+ $subsite = Subsite::where('api_key', $apiKey)
+ ->where('api_secret', $apiSecret)
+ ->where('status', Subsite::STATUS_ENABLED)
+ ->first();
+
+ if (!$subsite) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => 'API密钥无效'
+ ], 401);
+ }
+
+ // 获取商品列表
+ $goods = \App\Models\Goods::where('status', \App\Models\Goods::STATUS_OPEN)
+ ->with(['skus' => function($query) {
+ $query->where('status', \App\Models\GoodsSku::STATUS_ENABLED);
+ }])
+ ->get()
+ ->map(function($item) {
+ $data = [
+ 'id' => $item->id,
+ 'name' => $item->gd_name,
+ 'description' => $item->gd_description,
+ 'price' => $item->actual_price,
+ 'stock' => $item->in_stock,
+ 'type' => $item->type,
+ 'has_sku' => $item->has_sku,
+ 'picture' => $item->picture
+ ];
+
+ if ($item->has_sku && $item->skus->isNotEmpty()) {
+ $data['skus'] = $item->skus->map(function($sku) {
+ return [
+ 'id' => $sku->id,
+ 'sku_code' => $sku->sku_code,
+ 'name' => $sku->name,
+ 'price' => $sku->price,
+ 'stock' => $sku->stock,
+ 'attributes' => $sku->attributes
+ ];
+ });
+ }
+
+ return $data;
+ });
+
+ return response()->json([
+ 'status' => 'success',
+ 'data' => $goods,
+ 'total' => $goods->count()
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => '获取商品列表失败:' . $e->getMessage()
+ ], 500);
+ }
+ }
+
+ /**
+ * 获取订单状态
+ */
+ public function getOrderStatus(Request $request): JsonResponse
+ {
+ try {
+ // 验证API密钥
+ $apiKey = $request->header('Authorization');
+ $apiSecret = $request->header('X-API-Secret');
+
+ if (!$apiKey || !$apiSecret) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => 'API密钥缺失'
+ ], 401);
+ }
+
+ $apiKey = str_replace('Bearer ', '', $apiKey);
+
+ $subsite = Subsite::where('api_key', $apiKey)
+ ->where('api_secret', $apiSecret)
+ ->where('status', Subsite::STATUS_ENABLED)
+ ->first();
+
+ if (!$subsite) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => 'API密钥无效'
+ ], 401);
+ }
+
+ $orderSn = $request->input('order_sn');
+ if (!$orderSn) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => '订单号不能为空'
+ ], 400);
+ }
+
+ $order = Order::where('order_sn', $orderSn)->first();
+ if (!$order) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => '订单不存在'
+ ], 404);
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'data' => [
+ 'order_sn' => $order->order_sn,
+ 'status' => $order->status,
+ 'total_price' => $order->total_price,
+ 'created_at' => $order->created_at->toISOString(),
+ 'updated_at' => $order->updated_at->toISOString()
+ ]
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => '查询订单状态失败:' . $e->getMessage()
+ ], 500);
+ }
+ }
+}
diff --git a/app/Http/Controllers/ShoppingCartController.php b/app/Http/Controllers/ShoppingCartController.php
new file mode 100644
index 000000000..2343ec503
--- /dev/null
+++ b/app/Http/Controllers/ShoppingCartController.php
@@ -0,0 +1,382 @@
+cartService = $cartService;
+ }
+
+ /**
+ * 购物车页面
+ */
+ public function index(Request $request)
+ {
+ $sessionId = $request->session()->getId();
+ $userEmail = $request->input('email');
+
+ $cartData = ShoppingCart::getCartTotal($sessionId, $userEmail);
+
+ return view('cart.index', [
+ 'cartItems' => $cartData['items'],
+ 'totalQuantity' => $cartData['total_quantity'],
+ 'totalPrice' => $cartData['total_price'],
+ 'originalPrice' => $cartData['original_price'],
+ 'totalDiscount' => $cartData['total_discount'],
+ 'itemCount' => $cartData['item_count']
+ ]);
+ }
+
+ /**
+ * 添加商品到购物车
+ */
+ public function add(Request $request): JsonResponse
+ {
+ $validated = $request->validate([
+ 'goods_id' => 'required|exists:goods,id',
+ 'goods_sku_id' => 'nullable|exists:goods_skus,id',
+ 'quantity' => 'required|integer|min:1|max:999',
+ 'email' => 'nullable|email'
+ ]);
+
+ try {
+ $goods = Goods::findOrFail($validated['goods_id']);
+
+ // 检查商品状态
+ if ($goods->status !== Goods::STATUS_OPEN) {
+ return response()->json([
+ 'code' => 400,
+ 'message' => '商品已下架'
+ ]);
+ }
+
+ // 检查库存
+ $sku = null;
+ if ($validated['goods_sku_id']) {
+ $sku = GoodsSku::findOrFail($validated['goods_sku_id']);
+ if (!$sku->isAvailable()) {
+ return response()->json([
+ 'code' => 400,
+ 'message' => '商品规格不可用'
+ ]);
+ }
+
+ if ($sku->available_stock < $validated['quantity']) {
+ return response()->json([
+ 'code' => 400,
+ 'message' => '库存不足,当前库存:' . $sku->available_stock
+ ]);
+ }
+ } else {
+ if ($goods->in_stock < $validated['quantity']) {
+ return response()->json([
+ 'code' => 400,
+ 'message' => '库存不足,当前库存:' . $goods->in_stock
+ ]);
+ }
+ }
+
+ $cartData = [
+ 'session_id' => $request->session()->getId(),
+ 'user_email' => $validated['email'] ?? null,
+ 'goods_id' => $validated['goods_id'],
+ 'goods_sku_id' => $validated['goods_sku_id'] ?? null,
+ 'quantity' => $validated['quantity'],
+ 'price' => $sku ? $sku->price : $goods->actual_price,
+ 'goods_snapshot' => $goods->toArray(),
+ 'sku_snapshot' => $sku ? $sku->toArray() : null
+ ];
+
+ $cartItem = ShoppingCart::addToCart($cartData);
+
+ // 获取购物车统计
+ $cartTotal = ShoppingCart::getCartTotal($request->session()->getId(), $validated['email'] ?? null);
+
+ return response()->json([
+ 'code' => 200,
+ 'message' => '添加成功',
+ 'data' => [
+ 'cart_item' => $cartItem,
+ 'cart_total' => $cartTotal
+ ]
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '添加失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 更新购物车商品数量
+ */
+ public function update(Request $request, ShoppingCart $cartItem): JsonResponse
+ {
+ $validated = $request->validate([
+ 'quantity' => 'required|integer|min:1|max:999'
+ ]);
+
+ try {
+ // 验证会话
+ if ($cartItem->session_id !== $request->session()->getId()) {
+ return response()->json([
+ 'code' => 403,
+ 'message' => '无权限操作'
+ ]);
+ }
+
+ // 检查库存
+ $availableStock = $cartItem->goods_sku_id
+ ? $cartItem->goodsSku->available_stock
+ : $cartItem->goods->in_stock;
+
+ if ($availableStock < $validated['quantity']) {
+ return response()->json([
+ 'code' => 400,
+ 'message' => '库存不足,当前库存:' . $availableStock
+ ]);
+ }
+
+ $cartItem->updateQuantity($validated['quantity']);
+
+ // 获取购物车统计
+ $cartTotal = ShoppingCart::getCartTotal($request->session()->getId());
+
+ return response()->json([
+ 'code' => 200,
+ 'message' => '更新成功',
+ 'data' => [
+ 'cart_item' => $cartItem->fresh(),
+ 'cart_total' => $cartTotal
+ ]
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '更新失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 删除购物车商品
+ */
+ public function remove(Request $request, ShoppingCart $cartItem): JsonResponse
+ {
+ try {
+ // 验证会话
+ if ($cartItem->session_id !== $request->session()->getId()) {
+ return response()->json([
+ 'code' => 403,
+ 'message' => '无权限操作'
+ ]);
+ }
+
+ $cartItem->delete();
+
+ // 获取购物车统计
+ $cartTotal = ShoppingCart::getCartTotal($request->session()->getId());
+
+ return response()->json([
+ 'code' => 200,
+ 'message' => '删除成功',
+ 'data' => $cartTotal
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '删除失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 清空购物车
+ */
+ public function clear(Request $request): JsonResponse
+ {
+ try {
+ $sessionId = $request->session()->getId();
+ $userEmail = $request->input('email');
+
+ ShoppingCart::clearCart($sessionId, $userEmail);
+
+ return response()->json([
+ 'code' => 200,
+ 'message' => '购物车已清空'
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '清空失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 应用优惠券
+ */
+ public function applyCoupon(Request $request): JsonResponse
+ {
+ $validated = $request->validate([
+ 'coupon_code' => 'required|string',
+ 'cart_item_id' => 'required|exists:shopping_carts,id'
+ ]);
+
+ try {
+ $cartItem = ShoppingCart::findOrFail($validated['cart_item_id']);
+
+ // 验证会话
+ if ($cartItem->session_id !== $request->session()->getId()) {
+ return response()->json([
+ 'code' => 403,
+ 'message' => '无权限操作'
+ ]);
+ }
+
+ $result = $this->cartService->applyCoupon($cartItem, $validated['coupon_code']);
+
+ if ($result['success']) {
+ // 获取购物车统计
+ $cartTotal = ShoppingCart::getCartTotal($request->session()->getId());
+
+ return response()->json([
+ 'code' => 200,
+ 'message' => '优惠券应用成功',
+ 'data' => [
+ 'cart_item' => $cartItem->fresh(),
+ 'cart_total' => $cartTotal,
+ 'discount' => $result['discount']
+ ]
+ ]);
+ } else {
+ return response()->json([
+ 'code' => 400,
+ 'message' => $result['message']
+ ]);
+ }
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '应用失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 移除优惠券
+ */
+ public function removeCoupon(Request $request, ShoppingCart $cartItem): JsonResponse
+ {
+ try {
+ // 验证会话
+ if ($cartItem->session_id !== $request->session()->getId()) {
+ return response()->json([
+ 'code' => 403,
+ 'message' => '无权限操作'
+ ]);
+ }
+
+ $cartItem->removeCoupon();
+
+ // 获取购物车统计
+ $cartTotal = ShoppingCart::getCartTotal($request->session()->getId());
+
+ return response()->json([
+ 'code' => 200,
+ 'message' => '优惠券已移除',
+ 'data' => [
+ 'cart_item' => $cartItem->fresh(),
+ 'cart_total' => $cartTotal
+ ]
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '移除失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 获取购物车统计
+ */
+ public function getTotal(Request $request): JsonResponse
+ {
+ try {
+ $sessionId = $request->session()->getId();
+ $userEmail = $request->input('email');
+
+ $cartTotal = ShoppingCart::getCartTotal($sessionId, $userEmail);
+
+ return response()->json([
+ 'code' => 200,
+ 'data' => $cartTotal
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '获取失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 批量结算
+ */
+ public function checkout(Request $request): JsonResponse
+ {
+ $validated = $request->validate([
+ 'cart_item_ids' => 'required|array',
+ 'cart_item_ids.*' => 'exists:shopping_carts,id',
+ 'email' => 'required|email',
+ 'contact' => 'nullable|string'
+ ]);
+
+ try {
+ $result = $this->cartService->checkout(
+ $validated['cart_item_ids'],
+ $validated['email'],
+ $validated['contact'] ?? null,
+ $request->session()->getId()
+ );
+
+ if ($result['success']) {
+ return response()->json([
+ 'code' => 200,
+ 'message' => '订单创建成功',
+ 'data' => $result['orders']
+ ]);
+ } else {
+ return response()->json([
+ 'code' => 400,
+ 'message' => $result['message']
+ ]);
+ }
+ } catch (\Exception $e) {
+ return response()->json([
+ 'code' => 500,
+ 'message' => '结算失败:' . $e->getMessage()
+ ]);
+ }
+ }
+}
diff --git a/app/Models/Carmis.php b/app/Models/Carmis.php
index b0507ba39..c377f4b6d 100755
--- a/app/Models/Carmis.php
+++ b/app/Models/Carmis.php
@@ -52,4 +52,12 @@ public function goods()
return $this->belongsTo(Goods::class, 'goods_id');
}
+ /**
+ * 关联商品规格
+ */
+ public function goodsSku()
+ {
+ return $this->belongsTo(GoodsSku::class, 'goods_sku_id');
+ }
+
}
diff --git a/app/Models/CustomerService.php b/app/Models/CustomerService.php
new file mode 100644
index 000000000..28a4c59b4
--- /dev/null
+++ b/app/Models/CustomerService.php
@@ -0,0 +1,251 @@
+ 'array',
+ 'skills' => 'array',
+ 'is_enabled' => 'boolean'
+ ];
+
+ /**
+ * 获取状态映射
+ */
+ public static function getStatusMap(): array
+ {
+ return [
+ self::STATUS_OFFLINE => '离线',
+ self::STATUS_ONLINE => '在线',
+ self::STATUS_BUSY => '忙碌'
+ ];
+ }
+
+ /**
+ * 关联客服会话
+ */
+ public function sessions(): HasMany
+ {
+ return $this->hasMany(CustomerServiceSession::class, 'service_id');
+ }
+
+ /**
+ * 关联客服消息
+ */
+ public function messages(): HasMany
+ {
+ return $this->hasMany(CustomerServiceMessage::class, 'service_id');
+ }
+
+ /**
+ * 获取状态文本
+ */
+ public function getStatusTextAttribute(): string
+ {
+ return self::getStatusMap()[$this->status] ?? '';
+ }
+
+ /**
+ * 是否在线
+ */
+ public function isOnline(): bool
+ {
+ return $this->status === self::STATUS_ONLINE;
+ }
+
+ /**
+ * 是否忙碌
+ */
+ public function isBusy(): bool
+ {
+ return $this->status === self::STATUS_BUSY;
+ }
+
+ /**
+ * 是否可接受新会话
+ */
+ public function canAcceptNewSession(): bool
+ {
+ return $this->isOnline() &&
+ $this->current_sessions < $this->max_sessions &&
+ $this->is_enabled;
+ }
+
+ /**
+ * 增加当前会话数
+ */
+ public function incrementSessions(): bool
+ {
+ return $this->increment('current_sessions');
+ }
+
+ /**
+ * 减少当前会话数
+ */
+ public function decrementSessions(): bool
+ {
+ return $this->decrement('current_sessions');
+ }
+
+ /**
+ * 设置状态
+ */
+ public function setStatus(int $status): bool
+ {
+ return $this->update(['status' => $status]);
+ }
+
+ /**
+ * 上线
+ */
+ public function goOnline(): bool
+ {
+ return $this->setStatus(self::STATUS_ONLINE);
+ }
+
+ /**
+ * 下线
+ */
+ public function goOffline(): bool
+ {
+ return $this->setStatus(self::STATUS_OFFLINE);
+ }
+
+ /**
+ * 设为忙碌
+ */
+ public function setBusy(): bool
+ {
+ return $this->setStatus(self::STATUS_BUSY);
+ }
+
+ /**
+ * 获取技能列表
+ */
+ public function getSkillsList(): array
+ {
+ return $this->skills ?? [];
+ }
+
+ /**
+ * 是否有技能
+ */
+ public function hasSkill(string $skill): bool
+ {
+ return in_array($skill, $this->getSkillsList());
+ }
+
+ /**
+ * 添加技能
+ */
+ public function addSkill(string $skill): bool
+ {
+ $skills = $this->skills ?? [];
+ if (!in_array($skill, $skills)) {
+ $skills[] = $skill;
+ return $this->update(['skills' => $skills]);
+ }
+ return true;
+ }
+
+ /**
+ * 移除技能
+ */
+ public function removeSkill(string $skill): bool
+ {
+ $skills = $this->skills ?? [];
+ $key = array_search($skill, $skills);
+ if ($key !== false) {
+ unset($skills[$key]);
+ return $this->update(['skills' => array_values($skills)]);
+ }
+ return true;
+ }
+
+ /**
+ * 检查工作时间
+ */
+ public function isWorkingTime(): bool
+ {
+ if (!$this->working_hours) {
+ return true; // 没有设置工作时间则认为全天工作
+ }
+
+ $now = now();
+ $currentDay = strtolower($now->format('l')); // monday, tuesday, etc.
+ $currentTime = $now->format('H:i');
+
+ $daySchedule = $this->working_hours[$currentDay] ?? null;
+ if (!$daySchedule || !$daySchedule['enabled']) {
+ return false;
+ }
+
+ return $currentTime >= $daySchedule['start'] && $currentTime <= $daySchedule['end'];
+ }
+
+ /**
+ * 获取欢迎消息
+ */
+ public function getWelcomeMessage(): string
+ {
+ return $this->welcome_message ?? '您好,有什么可以帮助您的吗?';
+ }
+
+ /**
+ * 获取自动回复
+ */
+ public function getAutoReply(): ?string
+ {
+ return $this->auto_reply;
+ }
+
+ /**
+ * 获取当前会话负载率
+ */
+ public function getLoadRateAttribute(): float
+ {
+ if ($this->max_sessions <= 0) {
+ return 0;
+ }
+ return ($this->current_sessions / $this->max_sessions) * 100;
+ }
+}
diff --git a/app/Models/CustomerServiceMessage.php b/app/Models/CustomerServiceMessage.php
new file mode 100644
index 000000000..4817af5a4
--- /dev/null
+++ b/app/Models/CustomerServiceMessage.php
@@ -0,0 +1,287 @@
+ 'array',
+ 'is_auto_reply' => 'boolean',
+ 'read_at' => 'datetime'
+ ];
+
+ /**
+ * 获取发送者类型映射
+ */
+ public static function getSenderTypeMap(): array
+ {
+ return [
+ self::SENDER_USER => '用户',
+ self::SENDER_SERVICE => '客服',
+ self::SENDER_SYSTEM => '系统'
+ ];
+ }
+
+ /**
+ * 获取消息类型映射
+ */
+ public static function getMessageTypeMap(): array
+ {
+ return [
+ self::TYPE_TEXT => '文本',
+ self::TYPE_IMAGE => '图片',
+ self::TYPE_FILE => '文件',
+ self::TYPE_VOICE => '语音',
+ self::TYPE_VIDEO => '视频',
+ self::TYPE_SYSTEM => '系统消息'
+ ];
+ }
+
+ /**
+ * 获取状态映射
+ */
+ public static function getStatusMap(): array
+ {
+ return [
+ self::STATUS_UNREAD => '未读',
+ self::STATUS_READ => '已读'
+ ];
+ }
+
+ /**
+ * 关联会话
+ */
+ public function session(): BelongsTo
+ {
+ return $this->belongsTo(CustomerServiceSession::class, 'session_id');
+ }
+
+ /**
+ * 关联客服
+ */
+ public function service(): BelongsTo
+ {
+ return $this->belongsTo(CustomerService::class, 'service_id');
+ }
+
+ /**
+ * 关联回复的消息
+ */
+ public function replyTo(): BelongsTo
+ {
+ return $this->belongsTo(self::class, 'reply_to_id');
+ }
+
+ /**
+ * 关联回复消息
+ */
+ public function replies()
+ {
+ return $this->hasMany(self::class, 'reply_to_id');
+ }
+
+ /**
+ * 获取发送者类型文本
+ */
+ public function getSenderTypeTextAttribute(): string
+ {
+ return self::getSenderTypeMap()[$this->sender_type] ?? '';
+ }
+
+ /**
+ * 获取消息类型文本
+ */
+ public function getMessageTypeTextAttribute(): string
+ {
+ return self::getMessageTypeMap()[$this->message_type] ?? '';
+ }
+
+ /**
+ * 获取状态文本
+ */
+ public function getStatusTextAttribute(): string
+ {
+ return self::getStatusMap()[$this->status] ?? '';
+ }
+
+ /**
+ * 是否用户发送
+ */
+ public function isFromUser(): bool
+ {
+ return $this->sender_type === self::SENDER_USER;
+ }
+
+ /**
+ * 是否客服发送
+ */
+ public function isFromService(): bool
+ {
+ return $this->sender_type === self::SENDER_SERVICE;
+ }
+
+ /**
+ * 是否系统消息
+ */
+ public function isSystemMessage(): bool
+ {
+ return $this->sender_type === self::SENDER_SYSTEM;
+ }
+
+ /**
+ * 是否已读
+ */
+ public function isRead(): bool
+ {
+ return $this->status === self::STATUS_READ;
+ }
+
+ /**
+ * 是否自动回复
+ */
+ public function isAutoReply(): bool
+ {
+ return $this->is_auto_reply;
+ }
+
+ /**
+ * 标记为已读
+ */
+ public function markAsRead(): bool
+ {
+ return $this->update([
+ 'status' => self::STATUS_READ,
+ 'read_at' => now()
+ ]);
+ }
+
+ /**
+ * 获取附件列表
+ */
+ public function getAttachmentsList(): array
+ {
+ return $this->attachments ?? [];
+ }
+
+ /**
+ * 添加附件
+ */
+ public function addAttachment(array $attachment): bool
+ {
+ $attachments = $this->attachments ?? [];
+ $attachments[] = $attachment;
+ return $this->update(['attachments' => $attachments]);
+ }
+
+ /**
+ * 获取格式化内容
+ */
+ public function getFormattedContentAttribute(): string
+ {
+ switch ($this->message_type) {
+ case self::TYPE_IMAGE:
+ return '
';
+ case self::TYPE_FILE:
+ return '📎 文件下载';
+ case self::TYPE_VOICE:
+ return '🎵 语音消息';
+ case self::TYPE_VIDEO:
+ return '🎬 视频消息';
+ case self::TYPE_SYSTEM:
+ return '' . $this->content . '';
+ default:
+ return $this->content;
+ }
+ }
+
+ /**
+ * 创建文本消息
+ */
+ public static function createTextMessage(int $sessionId, int $senderType, string $content, ?int $serviceId = null, ?string $senderName = null): self
+ {
+ return self::create([
+ 'session_id' => $sessionId,
+ 'service_id' => $serviceId,
+ 'sender_type' => $senderType,
+ 'sender_name' => $senderName,
+ 'message_type' => self::TYPE_TEXT,
+ 'content' => $content,
+ 'status' => self::STATUS_UNREAD
+ ]);
+ }
+
+ /**
+ * 创建系统消息
+ */
+ public static function createSystemMessage(int $sessionId, string $content): self
+ {
+ return self::create([
+ 'session_id' => $sessionId,
+ 'sender_type' => self::SENDER_SYSTEM,
+ 'sender_name' => '系统',
+ 'message_type' => self::TYPE_SYSTEM,
+ 'content' => $content,
+ 'status' => self::STATUS_READ
+ ]);
+ }
+
+ /**
+ * 创建自动回复消息
+ */
+ public static function createAutoReply(int $sessionId, int $serviceId, string $content): self
+ {
+ return self::create([
+ 'session_id' => $sessionId,
+ 'service_id' => $serviceId,
+ 'sender_type' => self::SENDER_SERVICE,
+ 'message_type' => self::TYPE_TEXT,
+ 'content' => $content,
+ 'status' => self::STATUS_UNREAD,
+ 'is_auto_reply' => true
+ ]);
+ }
+}
diff --git a/app/Models/CustomerServiceSession.php b/app/Models/CustomerServiceSession.php
new file mode 100644
index 000000000..310ebb2e3
--- /dev/null
+++ b/app/Models/CustomerServiceSession.php
@@ -0,0 +1,284 @@
+ 'array',
+ 'started_at' => 'datetime',
+ 'ended_at' => 'datetime',
+ 'last_message_at' => 'datetime'
+ ];
+
+ /**
+ * 获取状态映射
+ */
+ public static function getStatusMap(): array
+ {
+ return [
+ self::STATUS_WAITING => '等待中',
+ self::STATUS_ACTIVE => '进行中',
+ self::STATUS_CLOSED => '已关闭'
+ ];
+ }
+
+ /**
+ * 获取来源映射
+ */
+ public static function getSourceMap(): array
+ {
+ return [
+ self::SOURCE_WEB => '网页',
+ self::SOURCE_MOBILE => '手机',
+ self::SOURCE_API => 'API'
+ ];
+ }
+
+ /**
+ * 关联客服
+ */
+ public function service(): BelongsTo
+ {
+ return $this->belongsTo(CustomerService::class, 'service_id');
+ }
+
+ /**
+ * 关联消息
+ */
+ public function messages(): HasMany
+ {
+ return $this->hasMany(CustomerServiceMessage::class, 'session_id');
+ }
+
+ /**
+ * 获取状态文本
+ */
+ public function getStatusTextAttribute(): string
+ {
+ return self::getStatusMap()[$this->status] ?? '';
+ }
+
+ /**
+ * 获取来源文本
+ */
+ public function getSourceTextAttribute(): string
+ {
+ return self::getSourceMap()[$this->source] ?? '';
+ }
+
+ /**
+ * 是否等待中
+ */
+ public function isWaiting(): bool
+ {
+ return $this->status === self::STATUS_WAITING;
+ }
+
+ /**
+ * 是否进行中
+ */
+ public function isActive(): bool
+ {
+ return $this->status === self::STATUS_ACTIVE;
+ }
+
+ /**
+ * 是否已关闭
+ */
+ public function isClosed(): bool
+ {
+ return $this->status === self::STATUS_CLOSED;
+ }
+
+ /**
+ * 开始会话
+ */
+ public function start(int $serviceId): bool
+ {
+ return $this->update([
+ 'service_id' => $serviceId,
+ 'status' => self::STATUS_ACTIVE,
+ 'started_at' => now()
+ ]);
+ }
+
+ /**
+ * 结束会话
+ */
+ public function end(): bool
+ {
+ return $this->update([
+ 'status' => self::STATUS_CLOSED,
+ 'ended_at' => now()
+ ]);
+ }
+
+ /**
+ * 更新最后消息时间
+ */
+ public function updateLastMessageTime(): bool
+ {
+ return $this->update(['last_message_at' => now()]);
+ }
+
+ /**
+ * 设置评分
+ */
+ public function setRating(int $rating, ?string $feedback = null): bool
+ {
+ return $this->update([
+ 'rating' => $rating,
+ 'feedback' => $feedback
+ ]);
+ }
+
+ /**
+ * 获取会话时长
+ */
+ public function getDurationAttribute(): ?int
+ {
+ if (!$this->started_at) {
+ return null;
+ }
+
+ $endTime = $this->ended_at ?? now();
+ return $this->started_at->diffInSeconds($endTime);
+ }
+
+ /**
+ * 获取格式化时长
+ */
+ public function getFormattedDurationAttribute(): string
+ {
+ $duration = $this->duration;
+ if (!$duration) {
+ return '0秒';
+ }
+
+ $hours = floor($duration / 3600);
+ $minutes = floor(($duration % 3600) / 60);
+ $seconds = $duration % 60;
+
+ $parts = [];
+ if ($hours > 0) $parts[] = $hours . '小时';
+ if ($minutes > 0) $parts[] = $minutes . '分钟';
+ if ($seconds > 0) $parts[] = $seconds . '秒';
+
+ return implode('', $parts);
+ }
+
+ /**
+ * 获取标签列表
+ */
+ public function getTagsList(): array
+ {
+ return $this->tags ?? [];
+ }
+
+ /**
+ * 添加标签
+ */
+ public function addTag(string $tag): bool
+ {
+ $tags = $this->tags ?? [];
+ if (!in_array($tag, $tags)) {
+ $tags[] = $tag;
+ return $this->update(['tags' => $tags]);
+ }
+ return true;
+ }
+
+ /**
+ * 移除标签
+ */
+ public function removeTag(string $tag): bool
+ {
+ $tags = $this->tags ?? [];
+ $key = array_search($tag, $tags);
+ if ($key !== false) {
+ unset($tags[$key]);
+ return $this->update(['tags' => array_values($tags)]);
+ }
+ return true;
+ }
+
+ /**
+ * 获取消息数量
+ */
+ public function getMessageCountAttribute(): int
+ {
+ return $this->messages()->count();
+ }
+
+ /**
+ * 获取用户消息数量
+ */
+ public function getUserMessageCountAttribute(): int
+ {
+ return $this->messages()->where('sender_type', CustomerServiceMessage::SENDER_USER)->count();
+ }
+
+ /**
+ * 获取客服消息数量
+ */
+ public function getServiceMessageCountAttribute(): int
+ {
+ return $this->messages()->where('sender_type', CustomerServiceMessage::SENDER_SERVICE)->count();
+ }
+
+ /**
+ * 是否超时
+ */
+ public function isTimeout(int $minutes = 30): bool
+ {
+ if (!$this->last_message_at) {
+ return false;
+ }
+
+ return $this->last_message_at->addMinutes($minutes)->isPast();
+ }
+}
diff --git a/app/Models/Goods.php b/app/Models/Goods.php
index a7022d522..c9660e48f 100755
--- a/app/Models/Goods.php
+++ b/app/Models/Goods.php
@@ -59,6 +59,30 @@ public function carmis()
return $this->hasMany(Carmis::class, 'goods_id');
}
+ /**
+ * 关联商品规格
+ */
+ public function skus()
+ {
+ return $this->hasMany(GoodsSku::class, 'goods_id');
+ }
+
+ /**
+ * 关联商品属性
+ */
+ public function attributes()
+ {
+ return $this->hasMany(GoodsAttribute::class, 'goods_id');
+ }
+
+ /**
+ * 关联购物车
+ */
+ public function cartItems()
+ {
+ return $this->hasMany(ShoppingCart::class, 'goods_id');
+ }
+
/**
* 库存读取器,将自动发货的库存更改为未出售卡密的数量
*
@@ -77,6 +101,92 @@ public function getInStockAttribute()
return $this->attributes['in_stock'];
}
+ /**
+ * 是否有多规格
+ */
+ public function hasSkus(): bool
+ {
+ return $this->has_sku == 1;
+ }
+
+ /**
+ * 获取启用的规格
+ */
+ public function getEnabledSkus()
+ {
+ return $this->skus()->enabled()->orderBy('sort', 'desc')->get();
+ }
+
+ /**
+ * 获取可用的规格
+ */
+ public function getAvailableSkus()
+ {
+ return $this->skus()->available()->orderBy('sort', 'desc')->get();
+ }
+
+ /**
+ * 获取价格范围
+ */
+ public function getPriceRange(): array
+ {
+ if (!$this->hasSkus()) {
+ return [
+ 'min' => $this->actual_price,
+ 'max' => $this->actual_price
+ ];
+ }
+
+ $skus = $this->getEnabledSkus();
+ if ($skus->isEmpty()) {
+ return [
+ 'min' => $this->actual_price,
+ 'max' => $this->actual_price
+ ];
+ }
+
+ return [
+ 'min' => $skus->min('price'),
+ 'max' => $skus->max('price')
+ ];
+ }
+
+ /**
+ * 更新价格范围
+ */
+ public function updatePriceRange(): bool
+ {
+ $range = $this->getPriceRange();
+ return $this->update([
+ 'min_price' => $range['min'],
+ 'max_price' => $range['max']
+ ]);
+ }
+
+ /**
+ * 获取总库存
+ */
+ public function getTotalStock(): int
+ {
+ if (!$this->hasSkus()) {
+ return $this->in_stock;
+ }
+
+ return $this->skus()->sum('stock');
+ }
+
+ /**
+ * 获取总销量
+ */
+ public function getTotalSales(): int
+ {
+ if (!$this->hasSkus()) {
+ return $this->sales_volume ?? 0;
+ }
+
+ return $this->skus()->sum('sold_count');
+ }
+
/**
* 获取组建映射
*
diff --git a/app/Models/GoodsAttribute.php b/app/Models/GoodsAttribute.php
new file mode 100644
index 000000000..484304ee1
--- /dev/null
+++ b/app/Models/GoodsAttribute.php
@@ -0,0 +1,269 @@
+ 'array',
+ 'validation_rules' => 'array',
+ 'is_required' => 'boolean',
+ 'is_filterable' => 'boolean',
+ 'is_searchable' => 'boolean'
+ ];
+
+ /**
+ * 获取属性类型映射
+ */
+ public static function getTypeMap(): array
+ {
+ return [
+ self::TYPE_TEXT => '文本',
+ self::TYPE_COLOR => '颜色',
+ self::TYPE_IMAGE => '图片',
+ self::TYPE_SIZE => '尺寸',
+ self::TYPE_NUMBER => '数字'
+ ];
+ }
+
+ /**
+ * 获取输入类型映射
+ */
+ public static function getInputTypeMap(): array
+ {
+ return [
+ self::INPUT_TYPE_RADIO => '单选',
+ self::INPUT_TYPE_CHECKBOX => '多选',
+ self::INPUT_TYPE_INPUT => '输入框',
+ self::INPUT_TYPE_SELECT => '下拉框'
+ ];
+ }
+
+ /**
+ * 关联商品
+ */
+ public function goods(): BelongsTo
+ {
+ return $this->belongsTo(Goods::class);
+ }
+
+ /**
+ * 获取类型文本
+ */
+ public function getTypeTextAttribute(): string
+ {
+ return self::getTypeMap()[$this->type] ?? '';
+ }
+
+ /**
+ * 获取输入类型文本
+ */
+ public function getInputTypeTextAttribute(): string
+ {
+ return self::getInputTypeMap()[$this->input_type] ?? '';
+ }
+
+ /**
+ * 是否必选
+ */
+ public function isRequired(): bool
+ {
+ return $this->is_required;
+ }
+
+ /**
+ * 是否可筛选
+ */
+ public function isFilterable(): bool
+ {
+ return $this->is_filterable;
+ }
+
+ /**
+ * 是否可搜索
+ */
+ public function isSearchable(): bool
+ {
+ return $this->is_searchable;
+ }
+
+ /**
+ * 获取属性值列表
+ */
+ public function getValuesList(): array
+ {
+ return $this->values ?? [];
+ }
+
+ /**
+ * 添加属性值
+ */
+ public function addValue(string $value): bool
+ {
+ $values = $this->values ?? [];
+ if (!in_array($value, $values)) {
+ $values[] = $value;
+ return $this->update(['values' => $values]);
+ }
+ return true;
+ }
+
+ /**
+ * 移除属性值
+ */
+ public function removeValue(string $value): bool
+ {
+ $values = $this->values ?? [];
+ $key = array_search($value, $values);
+ if ($key !== false) {
+ unset($values[$key]);
+ return $this->update(['values' => array_values($values)]);
+ }
+ return true;
+ }
+
+ /**
+ * 验证属性值
+ */
+ public function validateValue($value): bool
+ {
+ // 必选验证
+ if ($this->is_required && empty($value)) {
+ return false;
+ }
+
+ // 类型验证
+ switch ($this->type) {
+ case self::TYPE_NUMBER:
+ return is_numeric($value);
+ case self::TYPE_COLOR:
+ return preg_match('/^#[0-9A-Fa-f]{6}$/', $value);
+ case self::TYPE_IMAGE:
+ return filter_var($value, FILTER_VALIDATE_URL) !== false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * 获取验证规则
+ */
+ public function getValidationRules(): array
+ {
+ $rules = [];
+
+ if ($this->is_required) {
+ $rules[] = 'required';
+ }
+
+ switch ($this->type) {
+ case self::TYPE_NUMBER:
+ $rules[] = 'numeric';
+ break;
+ case self::TYPE_COLOR:
+ $rules[] = 'regex:/^#[0-9A-Fa-f]{6}$/';
+ break;
+ case self::TYPE_IMAGE:
+ $rules[] = 'url';
+ break;
+ }
+
+ // 自定义验证规则
+ if ($this->validation_rules) {
+ $rules = array_merge($rules, $this->validation_rules);
+ }
+
+ return $rules;
+ }
+
+ /**
+ * 格式化显示值
+ */
+ public function formatValue($value): string
+ {
+ switch ($this->type) {
+ case self::TYPE_COLOR:
+ return ' ' . $value;
+ case self::TYPE_IMAGE:
+ return '
';
+ case self::TYPE_NUMBER:
+ return $value . ($this->unit ? ' ' . $this->unit : '');
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * 获取默认值
+ */
+ public function getDefaultValue()
+ {
+ return $this->default_value;
+ }
+
+ /**
+ * 设置默认值
+ */
+ public function setDefaultValue($value): bool
+ {
+ return $this->update(['default_value' => $value]);
+ }
+
+ /**
+ * 获取属性描述
+ */
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ /**
+ * 获取单位
+ */
+ public function getUnit(): ?string
+ {
+ return $this->unit;
+ }
+}
diff --git a/app/Models/GoodsSku.php b/app/Models/GoodsSku.php
new file mode 100644
index 000000000..b17143ce7
--- /dev/null
+++ b/app/Models/GoodsSku.php
@@ -0,0 +1,324 @@
+ 'array',
+ 'price' => 'decimal:2',
+ 'wholesale_price' => 'decimal:2',
+ 'cost_price' => 'decimal:2',
+ 'weight' => 'decimal:2',
+ 'extra_data' => 'array'
+ ];
+
+ /**
+ * 获取状态映射
+ */
+ public static function getStatusMap(): array
+ {
+ return [
+ self::STATUS_DISABLED => '禁用',
+ self::STATUS_ENABLED => '启用'
+ ];
+ }
+
+ /**
+ * 关联商品
+ */
+ public function goods(): BelongsTo
+ {
+ return $this->belongsTo(Goods::class);
+ }
+
+ /**
+ * 关联卡密
+ */
+ public function carmis(): HasMany
+ {
+ return $this->hasMany(Carmis::class, 'goods_sku_id');
+ }
+
+ /**
+ * 关联购物车
+ */
+ public function cartItems(): HasMany
+ {
+ return $this->hasMany(ShoppingCart::class, 'goods_sku_id');
+ }
+
+ /**
+ * 关联订单
+ */
+ public function orders(): HasMany
+ {
+ return $this->hasMany(Order::class, 'goods_sku_id');
+ }
+
+ /**
+ * 启用的规格
+ */
+ public function scopeEnabled(Builder $query): Builder
+ {
+ return $query->where('status', self::STATUS_ENABLED);
+ }
+
+ /**
+ * 有库存的规格
+ */
+ public function scopeInStock(Builder $query): Builder
+ {
+ return $query->where('stock', '>', 0);
+ }
+
+ /**
+ * 可用的规格
+ */
+ public function scopeAvailable(Builder $query): Builder
+ {
+ return $query->enabled()->inStock();
+ }
+
+ /**
+ * 库存预警
+ */
+ public function scopeLowStock(Builder $query): Builder
+ {
+ return $query->whereRaw('stock <= warning_stock');
+ }
+
+ /**
+ * 获取状态文本
+ */
+ public function getStatusTextAttribute(): string
+ {
+ return self::getStatusMap()[$this->status] ?? '';
+ }
+
+ /**
+ * 获取可用库存
+ */
+ public function getAvailableStockAttribute(): int
+ {
+ if ($this->goods && $this->goods->type === Goods::AUTOMATIC_DELIVERY) {
+ return $this->carmis()->where('status', Carmis::STATUS_UNSOLD)->count();
+ }
+ return $this->stock;
+ }
+
+ /**
+ * 是否启用
+ */
+ public function isEnabled(): bool
+ {
+ return $this->status === self::STATUS_ENABLED;
+ }
+
+ /**
+ * 是否有库存
+ */
+ public function isInStock(): bool
+ {
+ return $this->available_stock > 0;
+ }
+
+ /**
+ * 是否可用
+ */
+ public function isAvailable(): bool
+ {
+ return $this->isEnabled() && $this->isInStock();
+ }
+
+ /**
+ * 是否库存预警
+ */
+ public function isLowStock(): bool
+ {
+ return $this->stock <= $this->warning_stock;
+ }
+
+ /**
+ * 减少库存
+ */
+ public function decreaseStock(int $quantity): bool
+ {
+ if ($this->stock < $quantity) {
+ return false;
+ }
+
+ return $this->update([
+ 'stock' => $this->stock - $quantity,
+ 'sold_count' => $this->sold_count + $quantity
+ ]);
+ }
+
+ /**
+ * 增加库存
+ */
+ public function increaseStock(int $quantity): bool
+ {
+ return $this->update([
+ 'stock' => $this->stock + $quantity
+ ]);
+ }
+
+ /**
+ * 获取属性值
+ */
+ public function getAttributeValue(string $attributeName): ?string
+ {
+ return $this->attributes[$attributeName] ?? null;
+ }
+
+ /**
+ * 是否有属性
+ */
+ public function hasAttribute(string $attributeName): bool
+ {
+ return isset($this->attributes[$attributeName]);
+ }
+
+ /**
+ * 获取格式化属性
+ */
+ public function getFormattedAttributes(): array
+ {
+ $formatted = [];
+ foreach ($this->attributes as $key => $value) {
+ $formatted[] = [
+ 'name' => $key,
+ 'value' => $value
+ ];
+ }
+ return $formatted;
+ }
+
+ /**
+ * 生成SKU编码
+ */
+ public static function generateSkuCode(int $goodsId): string
+ {
+ $prefix = 'SKU' . str_pad($goodsId, 6, '0', STR_PAD_LEFT);
+ $suffix = strtoupper(substr(md5(uniqid()), 0, 6));
+ return $prefix . $suffix;
+ }
+
+ /**
+ * 获取批发价格
+ */
+ public function getWholesalePrice(int $quantity): float
+ {
+ if (!$this->wholesale_price || $quantity < 2) {
+ return $this->price;
+ }
+
+ // 根据数量阶梯定价
+ if ($quantity >= 100) {
+ return $this->wholesale_price * 0.8;
+ } elseif ($quantity >= 50) {
+ return $this->wholesale_price * 0.9;
+ } elseif ($quantity >= 10) {
+ return $this->wholesale_price;
+ }
+
+ return $this->price;
+ }
+
+ /**
+ * 获取实际价格
+ */
+ public function getActualPrice(int $quantity = 1): float
+ {
+ return $this->getWholesalePrice($quantity);
+ }
+
+ /**
+ * 获取扩展数据
+ */
+ public function getExtraData(string $key = null)
+ {
+ if ($key) {
+ return $this->extra_data[$key] ?? null;
+ }
+ return $this->extra_data;
+ }
+
+ /**
+ * 设置扩展数据
+ */
+ public function setExtraData(string $key, $value): bool
+ {
+ $data = $this->extra_data ?? [];
+ $data[$key] = $value;
+ return $this->update(['extra_data' => $data]);
+ }
+
+ /**
+ * 获取利润率
+ */
+ public function getProfitMarginAttribute(): float
+ {
+ if (!$this->cost_price || $this->cost_price <= 0) {
+ return 0;
+ }
+ return (($this->price - $this->cost_price) / $this->cost_price) * 100;
+ }
+
+ /**
+ * 获取利润
+ */
+ public function getProfitAttribute(): float
+ {
+ return $this->price - ($this->cost_price ?? 0);
+ }
+
+ /**
+ * 获取带商品名称的规格名称
+ */
+ public function getNameWithGoodsAttribute(): string
+ {
+ return $this->goods->gd_name . ' - ' . $this->name;
+ }
+}
diff --git a/app/Models/Order.php b/app/Models/Order.php
index b03b3885a..5d54d6cbd 100755
--- a/app/Models/Order.php
+++ b/app/Models/Order.php
@@ -115,6 +115,22 @@ public function goods()
return $this->belongsTo(Goods::class, 'goods_id');
}
+ /**
+ * 关联商品规格
+ */
+ public function goodsSku()
+ {
+ return $this->belongsTo(GoodsSku::class, 'goods_sku_id');
+ }
+
+ /**
+ * 关联分站订单
+ */
+ public function subsiteOrders()
+ {
+ return $this->hasMany(SubsiteOrder::class, 'order_id');
+ }
+
/**
* 关联优惠券
*
diff --git a/app/Models/ShoppingCart.php b/app/Models/ShoppingCart.php
new file mode 100644
index 000000000..74ab50a6d
--- /dev/null
+++ b/app/Models/ShoppingCart.php
@@ -0,0 +1,307 @@
+ 'decimal:2',
+ 'total_price' => 'decimal:2',
+ 'discount_amount' => 'decimal:2',
+ 'goods_snapshot' => 'array',
+ 'sku_snapshot' => 'array',
+ 'custom_fields' => 'array',
+ 'expires_at' => 'datetime'
+ ];
+
+ /**
+ * 关联商品
+ */
+ public function goods(): BelongsTo
+ {
+ return $this->belongsTo(Goods::class);
+ }
+
+ /**
+ * 关联商品规格
+ */
+ public function goodsSku(): BelongsTo
+ {
+ return $this->belongsTo(GoodsSku::class);
+ }
+
+ /**
+ * 关联优惠券
+ */
+ public function coupon(): BelongsTo
+ {
+ return $this->belongsTo(Coupon::class, 'coupon_code', 'coupon');
+ }
+
+ /**
+ * 按会话ID查询
+ */
+ public function scopeBySession(Builder $query, string $sessionId): Builder
+ {
+ return $query->where('session_id', $sessionId);
+ }
+
+ /**
+ * 按邮箱查询
+ */
+ public function scopeByEmail(Builder $query, string $email): Builder
+ {
+ return $query->where('user_email', $email);
+ }
+
+ /**
+ * 未过期的购物车
+ */
+ public function scopeNotExpired(Builder $query): Builder
+ {
+ return $query->where(function ($q) {
+ $q->whereNull('expires_at')
+ ->orWhere('expires_at', '>', now());
+ });
+ }
+
+ /**
+ * 已过期的购物车
+ */
+ public function scopeExpired(Builder $query): Builder
+ {
+ return $query->where('expires_at', '<=', now());
+ }
+
+ /**
+ * 更新数量
+ */
+ public function updateQuantity(int $quantity): bool
+ {
+ $this->quantity = $quantity;
+ $this->total_price = ($this->price - $this->discount_amount) * $quantity;
+ return $this->save();
+ }
+
+ /**
+ * 增加数量
+ */
+ public function incrementQuantity(int $amount = 1): bool
+ {
+ return $this->updateQuantity($this->quantity + $amount);
+ }
+
+ /**
+ * 减少数量
+ */
+ public function decrementQuantity(int $amount = 1): bool
+ {
+ $newQuantity = max(1, $this->quantity - $amount);
+ return $this->updateQuantity($newQuantity);
+ }
+
+ /**
+ * 是否过期
+ */
+ public function isExpired(): bool
+ {
+ return $this->expires_at && $this->expires_at->isPast();
+ }
+
+ /**
+ * 延长过期时间
+ */
+ public function extend(int $hours = 24): bool
+ {
+ return $this->update([
+ 'expires_at' => now()->addHours($hours)
+ ]);
+ }
+
+ /**
+ * 应用优惠券
+ */
+ public function applyCoupon(string $couponCode): bool
+ {
+ $coupon = Coupon::where('coupon', $couponCode)
+ ->where('is_open', Coupon::STATUS_OPEN)
+ ->where('is_use', Coupon::STATUS_UNUSED)
+ ->where('ret', '>', 0)
+ ->first();
+
+ if (!$coupon) {
+ return false;
+ }
+
+ // 检查优惠券是否适用于该商品
+ if (!$coupon->goods->contains($this->goods_id)) {
+ return false;
+ }
+
+ $discountAmount = min($coupon->discount, $this->price);
+
+ return $this->update([
+ 'coupon_code' => $couponCode,
+ 'discount_amount' => $discountAmount,
+ 'total_price' => ($this->price - $discountAmount) * $this->quantity
+ ]);
+ }
+
+ /**
+ * 移除优惠券
+ */
+ public function removeCoupon(): bool
+ {
+ return $this->update([
+ 'coupon_code' => null,
+ 'discount_amount' => 0,
+ 'total_price' => $this->price * $this->quantity
+ ]);
+ }
+
+ /**
+ * 添加到购物车
+ */
+ public static function addToCart(array $data): self
+ {
+ $cart = self::where('session_id', $data['session_id'])
+ ->where('goods_id', $data['goods_id'])
+ ->where('goods_sku_id', $data['goods_sku_id'] ?? null)
+ ->first();
+
+ if ($cart) {
+ $cart->incrementQuantity($data['quantity'] ?? 1);
+ return $cart;
+ }
+
+ $data['total_price'] = $data['price'] * ($data['quantity'] ?? 1);
+ $data['expires_at'] = now()->addHours(24);
+
+ return self::create($data);
+ }
+
+ /**
+ * 获取购物车统计
+ */
+ public static function getCartTotal(string $sessionId, ?string $email = null): array
+ {
+ $query = self::bySession($sessionId)->notExpired();
+
+ if ($email) {
+ $query->where(function ($q) use ($email) {
+ $q->where('user_email', $email)
+ ->orWhereNull('user_email');
+ });
+ }
+
+ $items = $query->get();
+
+ return [
+ 'items' => $items,
+ 'total_quantity' => $items->sum('quantity'),
+ 'total_price' => $items->sum('total_price'),
+ 'original_price' => $items->sum(function($item) {
+ return $item->price * $item->quantity;
+ }),
+ 'total_discount' => $items->sum(function($item) {
+ return $item->discount_amount * $item->quantity;
+ }),
+ 'item_count' => $items->count()
+ ];
+ }
+
+ /**
+ * 清理过期购物车
+ */
+ public static function clearExpired(): int
+ {
+ return self::expired()->delete();
+ }
+
+ /**
+ * 清空购物车
+ */
+ public static function clearCart(string $sessionId, ?string $email = null): int
+ {
+ $query = self::bySession($sessionId);
+
+ if ($email) {
+ $query->where('user_email', $email);
+ }
+
+ return $query->delete();
+ }
+
+ /**
+ * 获取商品快照
+ */
+ public function getGoodsSnapshot(string $key = null)
+ {
+ if ($key) {
+ return $this->goods_snapshot[$key] ?? null;
+ }
+ return $this->goods_snapshot;
+ }
+
+ /**
+ * 获取SKU快照
+ */
+ public function getSkuSnapshot(string $key = null)
+ {
+ if ($key) {
+ return $this->sku_snapshot[$key] ?? null;
+ }
+ return $this->sku_snapshot;
+ }
+
+ /**
+ * 获取自定义字段
+ */
+ public function getCustomField(string $key = null)
+ {
+ if ($key) {
+ return $this->custom_fields[$key] ?? null;
+ }
+ return $this->custom_fields;
+ }
+
+ /**
+ * 设置自定义字段
+ */
+ public function setCustomField(string $key, $value): bool
+ {
+ $fields = $this->custom_fields ?? [];
+ $fields[$key] = $value;
+ return $this->update(['custom_fields' => $fields]);
+ }
+}
diff --git a/app/Models/Subsite.php b/app/Models/Subsite.php
new file mode 100644
index 000000000..ee51e3db1
--- /dev/null
+++ b/app/Models/Subsite.php
@@ -0,0 +1,204 @@
+ 'array',
+ 'api_config' => 'array',
+ 'commission_rate' => 'decimal:2',
+ 'balance' => 'decimal:2',
+ 'last_sync_at' => 'datetime'
+ ];
+
+ /**
+ * 获取分站类型映射
+ */
+ public static function getTypeMap(): array
+ {
+ return [
+ self::TYPE_LOCAL => '本站分站',
+ self::TYPE_THIRD_PARTY => '第三方对接'
+ ];
+ }
+
+ /**
+ * 获取状态映射
+ */
+ public static function getStatusMap(): array
+ {
+ return [
+ self::STATUS_DISABLED => '禁用',
+ self::STATUS_ENABLED => '启用'
+ ];
+ }
+
+ /**
+ * 关联分站订单
+ */
+ public function orders(): HasMany
+ {
+ return $this->hasMany(SubsiteOrder::class);
+ }
+
+ /**
+ * 获取类型文本
+ */
+ public function getTypeTextAttribute(): string
+ {
+ return self::getTypeMap()[$this->type] ?? '';
+ }
+
+ /**
+ * 获取状态文本
+ */
+ public function getStatusTextAttribute(): string
+ {
+ return self::getStatusMap()[$this->status] ?? '';
+ }
+
+ /**
+ * 是否启用
+ */
+ public function isEnabled(): bool
+ {
+ return $this->status === self::STATUS_ENABLED;
+ }
+
+ /**
+ * 是否本站分站
+ */
+ public function isLocal(): bool
+ {
+ return $this->type === self::TYPE_LOCAL;
+ }
+
+ /**
+ * 是否第三方对接
+ */
+ public function isThirdParty(): bool
+ {
+ return $this->type === self::TYPE_THIRD_PARTY;
+ }
+
+ /**
+ * 增加余额
+ */
+ public function addBalance(float $amount): bool
+ {
+ return $this->increment('balance', $amount);
+ }
+
+ /**
+ * 扣减余额
+ */
+ public function deductBalance(float $amount): bool
+ {
+ if ($this->balance < $amount) {
+ return false;
+ }
+ return $this->decrement('balance', $amount);
+ }
+
+ /**
+ * 更新最后同步时间
+ */
+ public function updateLastSyncTime(): bool
+ {
+ return $this->update(['last_sync_at' => now()]);
+ }
+
+ /**
+ * 计算佣金
+ */
+ public function calculateCommission(float $orderAmount): float
+ {
+ return $orderAmount * ($this->commission_rate / 100);
+ }
+
+ /**
+ * 获取API配置
+ */
+ public function getApiConfig(string $key = null)
+ {
+ if ($key) {
+ return $this->api_config[$key] ?? null;
+ }
+ return $this->api_config;
+ }
+
+ /**
+ * 设置API配置
+ */
+ public function setApiConfig(string $key, $value): bool
+ {
+ $config = $this->api_config ?? [];
+ $config[$key] = $value;
+ return $this->update(['api_config' => $config]);
+ }
+
+ /**
+ * 获取分站设置
+ */
+ public function getSetting(string $key = null)
+ {
+ if ($key) {
+ return $this->settings[$key] ?? null;
+ }
+ return $this->settings;
+ }
+
+ /**
+ * 设置分站配置
+ */
+ public function setSetting(string $key, $value): bool
+ {
+ $settings = $this->settings ?? [];
+ $settings[$key] = $value;
+ return $this->update(['settings' => $settings]);
+ }
+}
diff --git a/app/Models/SubsiteOrder.php b/app/Models/SubsiteOrder.php
new file mode 100644
index 000000000..854cd0aed
--- /dev/null
+++ b/app/Models/SubsiteOrder.php
@@ -0,0 +1,206 @@
+ 'array',
+ 'commission_amount' => 'decimal:2',
+ 'commission_settled_at' => 'datetime',
+ 'synced_at' => 'datetime',
+ 'next_retry_at' => 'datetime'
+ ];
+
+ /**
+ * 获取佣金状态映射
+ */
+ public static function getCommissionStatusMap(): array
+ {
+ return [
+ self::COMMISSION_STATUS_PENDING => '未结算',
+ self::COMMISSION_STATUS_SETTLED => '已结算'
+ ];
+ }
+
+ /**
+ * 获取同步状态映射
+ */
+ public static function getSyncStatusMap(): array
+ {
+ return [
+ self::SYNC_STATUS_PENDING => '未同步',
+ self::SYNC_STATUS_SUCCESS => '已同步',
+ self::SYNC_STATUS_FAILED => '同步失败'
+ ];
+ }
+
+ /**
+ * 关联分站
+ */
+ public function subsite(): BelongsTo
+ {
+ return $this->belongsTo(Subsite::class);
+ }
+
+ /**
+ * 关联订单
+ */
+ public function order(): BelongsTo
+ {
+ return $this->belongsTo(Order::class);
+ }
+
+ /**
+ * 获取佣金状态文本
+ */
+ public function getCommissionStatusTextAttribute(): string
+ {
+ return self::getCommissionStatusMap()[$this->commission_status] ?? '';
+ }
+
+ /**
+ * 获取同步状态文本
+ */
+ public function getSyncStatusTextAttribute(): string
+ {
+ return self::getSyncStatusMap()[$this->sync_status] ?? '';
+ }
+
+ /**
+ * 是否佣金已结算
+ */
+ public function isCommissionSettled(): bool
+ {
+ return $this->commission_status === self::COMMISSION_STATUS_SETTLED;
+ }
+
+ /**
+ * 是否已同步
+ */
+ public function isSynced(): bool
+ {
+ return $this->sync_status === self::SYNC_STATUS_SUCCESS;
+ }
+
+ /**
+ * 标记佣金已结算
+ */
+ public function markCommissionSettled(): bool
+ {
+ return $this->update([
+ 'commission_status' => self::COMMISSION_STATUS_SETTLED,
+ 'commission_settled_at' => now()
+ ]);
+ }
+
+ /**
+ * 标记同步成功
+ */
+ public function markSyncSuccess(): bool
+ {
+ return $this->update([
+ 'sync_status' => self::SYNC_STATUS_SUCCESS,
+ 'synced_at' => now(),
+ 'sync_error' => null,
+ 'retry_count' => 0,
+ 'next_retry_at' => null
+ ]);
+ }
+
+ /**
+ * 标记同步失败
+ */
+ public function markSyncFailed(string $error): bool
+ {
+ $retryCount = $this->retry_count + 1;
+ $nextRetryAt = now()->addMinutes(pow(2, $retryCount)); // 指数退避
+
+ return $this->update([
+ 'sync_status' => self::SYNC_STATUS_FAILED,
+ 'synced_at' => now(),
+ 'sync_error' => $error,
+ 'retry_count' => $retryCount,
+ 'next_retry_at' => $nextRetryAt
+ ]);
+ }
+
+ /**
+ * 重置重试
+ */
+ public function resetRetry(): bool
+ {
+ return $this->update([
+ 'retry_count' => 0,
+ 'next_retry_at' => null,
+ 'sync_error' => null
+ ]);
+ }
+
+ /**
+ * 是否可以重试
+ */
+ public function canRetry(): bool
+ {
+ return $this->retry_count < 5 &&
+ ($this->next_retry_at === null || $this->next_retry_at->isPast());
+ }
+
+ /**
+ * 获取同步数据
+ */
+ public function getSyncData(string $key = null)
+ {
+ if ($key) {
+ return $this->sync_data[$key] ?? null;
+ }
+ return $this->sync_data;
+ }
+
+ /**
+ * 设置同步数据
+ */
+ public function setSyncData(string $key, $value): bool
+ {
+ $data = $this->sync_data ?? [];
+ $data[$key] = $value;
+ return $this->update(['sync_data' => $data]);
+ }
+}
diff --git a/app/Service/CarmisService.php b/app/Service/CarmisService.php
index b8c285b7d..ef2cb6de3 100644
--- a/app/Service/CarmisService.php
+++ b/app/Service/CarmisService.php
@@ -30,6 +30,24 @@ public function withGoodsByAmountAndStatusUnsold(int $goodsID, int $byAmount)
{
$carmis = Carmis::query()
->where('goods_id', $goodsID)
+ ->whereNull('goods_sku_id') // 只查询没有SKU的卡密
+ ->where('status', Carmis::STATUS_UNSOLD)
+ ->take($byAmount)
+ ->get();
+ return $carmis ? $carmis->toArray() : null;
+ }
+
+ /**
+ * 通过商品规格查询一些数量未使用的卡密
+ *
+ * @param int $skuID 商品规格id
+ * @param int $byAmount 数量
+ * @return array|null
+ */
+ public function withSkuByAmountAndStatusUnsold(int $skuID, int $byAmount)
+ {
+ $carmis = Carmis::query()
+ ->where('goods_sku_id', $skuID)
->where('status', Carmis::STATUS_UNSOLD)
->take($byAmount)
->get();
diff --git a/app/Service/OrderProcessService.php b/app/Service/OrderProcessService.php
index 8b8f143d1..54b900da5 100644
--- a/app/Service/OrderProcessService.php
+++ b/app/Service/OrderProcessService.php
@@ -76,6 +76,12 @@ class OrderProcessService
*/
private $goods;
+ /**
+ * 商品规格
+ * @var \App\Models\GoodsSku
+ */
+ private $goodsSku;
+
/**
* 优惠码
* @var Coupon;
@@ -189,6 +195,16 @@ public function setGoods(Goods $goods)
$this->goods = $goods;
}
+ /**
+ * 设置商品规格
+ *
+ * @param \App\Models\GoodsSku|null $goodsSku
+ */
+ public function setGoodsSku(?\App\Models\GoodsSku $goodsSku)
+ {
+ $this->goodsSku = $goodsSku;
+ }
+
/**
* 设置优惠码.
*
@@ -274,7 +290,7 @@ private function calculateTheWholesalePrice(): float
*/
private function calculateTheTotalPrice(): float
{
- $price = $this->goods->actual_price;
+ $price = $this->goodsSku ? $this->goodsSku->price : $this->goods->actual_price;
return bcmul($price, $this->buyAmount, 2);
}
@@ -317,8 +333,17 @@ public function createOrder(): Order
$order->order_sn = strtoupper(Str::random(16));
// 设置商品
$order->goods_id = $this->goods->id;
+ // 设置商品规格
+ if ($this->goodsSku) {
+ $order->goods_sku_id = $this->goodsSku->id;
+ $order->sku_snapshot = json_encode($this->goodsSku->toArray());
+ }
// 标题
- $order->title = $this->goods->gd_name . ' x ' . $this->buyAmount;
+ $title = $this->goods->gd_name;
+ if ($this->goodsSku) {
+ $title .= ' (' . $this->goodsSku->name . ')';
+ }
+ $order->title = $title . ' x ' . $this->buyAmount;
// 订单类型
$order->type = $this->goods->type;
// 查询密码
@@ -328,7 +353,7 @@ public function createOrder(): Order
// 支付方式.
$order->pay_id = $this->payID;
// 商品单价
- $order->goods_price = $this->goods->actual_price;
+ $order->goods_price = $this->goodsSku ? $this->goodsSku->price : $this->goods->actual_price;
// 购买数量
$order->buy_amount = $this->buyAmount;
// 订单详情
@@ -359,6 +384,14 @@ public function createOrder(): Order
// 使用次数-1
$this->couponService->retDecr($this->coupon->coupon);
}
+
+ // 减少库存
+ if ($this->goodsSku) {
+ $this->goodsSku->decreaseStock($this->buyAmount);
+ } else {
+ $this->goods->decrement('in_stock', $this->buyAmount);
+ $this->goods->increment('sales_volume', $this->buyAmount);
+ }
// 将订单加入队列 x分钟后过期
$expiredOrderDate = dujiaoka_config_get('order_expire_time', 5);
OrderExpired::dispatch($order->order_sn)->delay(Carbon::now()->addMinutes($expiredOrderDate));
@@ -430,6 +463,13 @@ public function completedOrder(string $orderSN, float $actualPrice, string $trad
}
// 回调事件
ApiHook::dispatch($order);
+
+ // 创建分站订单记录
+ if (class_exists('\App\Services\SubsiteService')) {
+ $subsiteService = app('\App\Services\SubsiteService');
+ $subsiteService->createSubsiteOrder($completedOrder);
+ }
+
return $completedOrder;
} catch (\Exception $exception) {
DB::rollBack();
@@ -489,7 +529,13 @@ public function processManual(Order $order)
public function processAuto(Order $order): Order
{
// 获得卡密
- $carmis = $this->carmisService->withGoodsByAmountAndStatusUnsold($order->goods_id, $order->buy_amount);
+ if ($order->goods_sku_id) {
+ // 如果有SKU,获取对应SKU的卡密
+ $carmis = $this->carmisService->withSkuByAmountAndStatusUnsold($order->goods_sku_id, $order->buy_amount);
+ } else {
+ // 没有SKU,获取商品的卡密
+ $carmis = $this->carmisService->withGoodsByAmountAndStatusUnsold($order->goods_id, $order->buy_amount);
+ }
// 实际可使用的库存已经少于购买数量了
if (count($carmis) != $order->buy_amount) {
$order->info = __('dujiaoka.prompt.order_carmis_insufficient_quantity_available');
diff --git a/app/Services/ShoppingCartService.php b/app/Services/ShoppingCartService.php
new file mode 100644
index 000000000..0115ecb66
--- /dev/null
+++ b/app/Services/ShoppingCartService.php
@@ -0,0 +1,345 @@
+subsiteService = $subsiteService;
+ }
+
+ /**
+ * 应用优惠券
+ */
+ public function applyCoupon(ShoppingCart $cartItem, string $couponCode): array
+ {
+ $coupon = Coupon::where('coupon', $couponCode)
+ ->where('is_open', Coupon::STATUS_OPEN)
+ ->where('is_use', Coupon::STATUS_UNUSED)
+ ->where('ret', '>', 0)
+ ->first();
+
+ if (!$coupon) {
+ return [
+ 'success' => false,
+ 'message' => '优惠券不存在或已失效'
+ ];
+ }
+
+ // 检查优惠券是否适用于该商品
+ if (!$coupon->goods->contains($cartItem->goods_id)) {
+ return [
+ 'success' => false,
+ 'message' => '优惠券不适用于该商品'
+ ];
+ }
+
+ // 检查优惠券使用条件
+ if ($coupon->min_amount && $cartItem->total_price < $coupon->min_amount) {
+ return [
+ 'success' => false,
+ 'message' => '订单金额不满足优惠券使用条件,最低需要 ¥' . $coupon->min_amount
+ ];
+ }
+
+ $discountAmount = min($coupon->discount, $cartItem->price);
+
+ $cartItem->update([
+ 'coupon_code' => $couponCode,
+ 'discount_amount' => $discountAmount,
+ 'total_price' => ($cartItem->price - $discountAmount) * $cartItem->quantity
+ ]);
+
+ return [
+ 'success' => true,
+ 'message' => '优惠券应用成功',
+ 'discount' => $discountAmount
+ ];
+ }
+
+ /**
+ * 批量结算
+ */
+ public function checkout(array $cartItemIds, string $email, ?string $contact, string $sessionId): array
+ {
+ return DB::transaction(function () use ($cartItemIds, $email, $contact, $sessionId) {
+ $cartItems = ShoppingCart::whereIn('id', $cartItemIds)
+ ->where('session_id', $sessionId)
+ ->with(['goods', 'goodsSku', 'coupon'])
+ ->get();
+
+ if ($cartItems->isEmpty()) {
+ return [
+ 'success' => false,
+ 'message' => '购物车为空'
+ ];
+ }
+
+ $orders = [];
+ $errors = [];
+
+ foreach ($cartItems as $cartItem) {
+ try {
+ // 检查商品状态和库存
+ $checkResult = $this->checkCartItemAvailability($cartItem);
+ if (!$checkResult['available']) {
+ $errors[] = $checkResult['message'];
+ continue;
+ }
+
+ // 创建订单
+ $order = $this->createOrderFromCartItem($cartItem, $email, $contact);
+
+ if ($order) {
+ $orders[] = $order;
+
+ // 减少库存
+ $this->decreaseStock($cartItem);
+
+ // 使用优惠券
+ if ($cartItem->coupon_code) {
+ $this->useCoupon($cartItem->coupon_code);
+ }
+
+ // 创建分站订单记录
+ $this->subsiteService->createSubsiteOrder($order);
+
+ // 删除购物车项
+ $cartItem->delete();
+ }
+ } catch (\Exception $e) {
+ $errors[] = '商品 "' . $cartItem->goods->gd_name . '" 结算失败:' . $e->getMessage();
+ }
+ }
+
+ if (empty($orders)) {
+ return [
+ 'success' => false,
+ 'message' => '所有商品结算失败:' . implode('; ', $errors)
+ ];
+ }
+
+ return [
+ 'success' => true,
+ 'orders' => $orders,
+ 'errors' => $errors
+ ];
+ });
+ }
+
+ /**
+ * 检查购物车项可用性
+ */
+ protected function checkCartItemAvailability(ShoppingCart $cartItem): array
+ {
+ $goods = $cartItem->goods;
+
+ // 检查商品状态
+ if ($goods->status !== $goods::STATUS_OPEN) {
+ return [
+ 'available' => false,
+ 'message' => '商品 "' . $goods->gd_name . '" 已下架'
+ ];
+ }
+
+ // 检查库存
+ if ($cartItem->goods_sku_id) {
+ $sku = $cartItem->goodsSku;
+ if (!$sku || !$sku->isAvailable()) {
+ return [
+ 'available' => false,
+ 'message' => '商品规格 "' . ($sku->name ?? '未知') . '" 不可用'
+ ];
+ }
+
+ if ($sku->available_stock < $cartItem->quantity) {
+ return [
+ 'available' => false,
+ 'message' => '商品规格 "' . $sku->name . '" 库存不足'
+ ];
+ }
+ } else {
+ if ($goods->in_stock < $cartItem->quantity) {
+ return [
+ 'available' => false,
+ 'message' => '商品 "' . $goods->gd_name . '" 库存不足'
+ ];
+ }
+ }
+
+ return ['available' => true];
+ }
+
+ /**
+ * 从购物车项创建订单
+ */
+ protected function createOrderFromCartItem(ShoppingCart $cartItem, string $email, ?string $contact): Order
+ {
+ $goods = $cartItem->goods;
+ $sku = $cartItem->goodsSku;
+
+ $orderData = [
+ 'order_sn' => $this->generateOrderSn(),
+ 'goods_id' => $cartItem->goods_id,
+ 'goods_sku_id' => $cartItem->goods_sku_id,
+ 'email' => $email,
+ 'contact' => $contact ?? '',
+ 'buy_amount' => $cartItem->quantity,
+ 'actual_price' => $cartItem->price - $cartItem->discount_amount,
+ 'total_price' => $cartItem->total_price,
+ 'coupon_discount_price' => $cartItem->discount_amount * $cartItem->quantity,
+ 'status' => Order::STATUS_PENDING,
+ 'info' => json_encode($cartItem->goods_snapshot),
+ 'sku_snapshot' => $cartItem->sku_snapshot ? json_encode($cartItem->sku_snapshot) : null
+ ];
+
+ // 如果有优惠券,记录优惠券信息
+ if ($cartItem->coupon_code) {
+ $orderData['coupon'] = $cartItem->coupon_code;
+ }
+
+ return Order::create($orderData);
+ }
+
+ /**
+ * 减少库存
+ */
+ protected function decreaseStock(ShoppingCart $cartItem): void
+ {
+ if ($cartItem->goods_sku_id) {
+ // 减少SKU库存
+ $cartItem->goodsSku->decreaseStock($cartItem->quantity);
+ } else {
+ // 减少商品库存
+ $goods = $cartItem->goods;
+ $goods->decrement('in_stock', $cartItem->quantity);
+ $goods->increment('sales_volume', $cartItem->quantity);
+ }
+ }
+
+ /**
+ * 使用优惠券
+ */
+ protected function useCoupon(string $couponCode): void
+ {
+ $coupon = Coupon::where('coupon', $couponCode)->first();
+ if ($coupon) {
+ $coupon->decrement('ret');
+ if ($coupon->ret <= 0) {
+ $coupon->update(['is_use' => Coupon::STATUS_USED]);
+ }
+ }
+ }
+
+ /**
+ * 生成订单号
+ */
+ protected function generateOrderSn(): string
+ {
+ do {
+ $orderSn = date('YmdHis') . mt_rand(100000, 999999);
+ } while (Order::where('order_sn', $orderSn)->exists());
+
+ return $orderSn;
+ }
+
+ /**
+ * 清理过期购物车
+ */
+ public function clearExpiredCarts(): int
+ {
+ return ShoppingCart::expired()->delete();
+ }
+
+ /**
+ * 获取推荐商品
+ */
+ public function getRecommendedGoods(ShoppingCart $cartItem, int $limit = 5): array
+ {
+ $goods = $cartItem->goods;
+
+ // 基于商品分组推荐
+ $recommendedGoods = $goods->goodsGroup->goods()
+ ->where('id', '!=', $goods->id)
+ ->where('status', $goods::STATUS_OPEN)
+ ->where('in_stock', '>', 0)
+ ->orderBy('sales_volume', 'desc')
+ ->limit($limit)
+ ->get();
+
+ return $recommendedGoods->toArray();
+ }
+
+ /**
+ * 计算购物车优惠
+ */
+ public function calculateCartDiscount(array $cartItems): array
+ {
+ $totalDiscount = 0;
+ $appliedCoupons = [];
+
+ foreach ($cartItems as $cartItem) {
+ if ($cartItem->discount_amount > 0) {
+ $totalDiscount += $cartItem->discount_amount * $cartItem->quantity;
+ if ($cartItem->coupon_code && !in_array($cartItem->coupon_code, $appliedCoupons)) {
+ $appliedCoupons[] = $cartItem->coupon_code;
+ }
+ }
+ }
+
+ return [
+ 'total_discount' => $totalDiscount,
+ 'applied_coupons' => $appliedCoupons,
+ 'coupon_count' => count($appliedCoupons)
+ ];
+ }
+
+ /**
+ * 验证购物车完整性
+ */
+ public function validateCart(string $sessionId, ?string $email = null): array
+ {
+ $cartItems = ShoppingCart::bySession($sessionId)
+ ->notExpired()
+ ->with(['goods', 'goodsSku'])
+ ->get();
+
+ $validItems = [];
+ $invalidItems = [];
+
+ foreach ($cartItems as $cartItem) {
+ $checkResult = $this->checkCartItemAvailability($cartItem);
+ if ($checkResult['available']) {
+ $validItems[] = $cartItem;
+ } else {
+ $invalidItems[] = [
+ 'item' => $cartItem,
+ 'reason' => $checkResult['message']
+ ];
+ }
+ }
+
+ return [
+ 'valid_items' => $validItems,
+ 'invalid_items' => $invalidItems,
+ 'total_valid' => count($validItems),
+ 'total_invalid' => count($invalidItems)
+ ];
+ }
+}
diff --git a/app/Services/SubsiteService.php b/app/Services/SubsiteService.php
new file mode 100644
index 000000000..ac0ed789b
--- /dev/null
+++ b/app/Services/SubsiteService.php
@@ -0,0 +1,335 @@
+generateApiKey();
+ $data['api_secret'] = $this->generateApiSecret();
+ }
+
+ // 设置默认配置
+ $data['settings'] = [
+ 'auto_sync' => true,
+ 'sync_interval' => 300, // 5分钟
+ 'max_retry' => 5,
+ 'timeout' => 30
+ ];
+
+ return Subsite::create($data);
+ });
+ }
+
+ /**
+ * 更新分站
+ */
+ public function updateSubsite(Subsite $subsite, array $data): bool
+ {
+ return DB::transaction(function () use ($subsite, $data) {
+ // 如果是本站分站且没有API密钥,则生成
+ if ($data['type'] == Subsite::TYPE_LOCAL && empty($subsite->api_key)) {
+ $data['api_key'] = $this->generateApiKey();
+ $data['api_secret'] = $this->generateApiSecret();
+ }
+
+ return $subsite->update($data);
+ });
+ }
+
+ /**
+ * 删除分站
+ */
+ public function deleteSubsite(Subsite $subsite): bool
+ {
+ return DB::transaction(function () use ($subsite) {
+ // 删除相关订单记录
+ $subsite->orders()->delete();
+
+ return $subsite->delete();
+ });
+ }
+
+ /**
+ * 佣金结算
+ */
+ public function settleCommissions(array $orderIds): array
+ {
+ return DB::transaction(function () use ($orderIds) {
+ $subsiteOrders = SubsiteOrder::whereIn('id', $orderIds)
+ ->where('commission_status', SubsiteOrder::COMMISSION_STATUS_PENDING)
+ ->with('subsite')
+ ->get();
+
+ $totalAmount = 0;
+ $settledCount = 0;
+
+ foreach ($subsiteOrders as $subsiteOrder) {
+ if ($subsiteOrder->markCommissionSettled()) {
+ // 增加分站余额
+ $subsiteOrder->subsite->addBalance($subsiteOrder->commission_amount);
+ $totalAmount += $subsiteOrder->commission_amount;
+ $settledCount++;
+ }
+ }
+
+ return [
+ 'settled_count' => $settledCount,
+ 'total_amount' => $totalAmount
+ ];
+ });
+ }
+
+ /**
+ * 同步订单到分站
+ */
+ public function syncOrdersToSubsite(Subsite $subsite): array
+ {
+ if (!$subsite->isThirdParty()) {
+ throw new \Exception('只有第三方分站才能同步订单');
+ }
+
+ $pendingOrders = $subsite->orders()
+ ->where('sync_status', SubsiteOrder::SYNC_STATUS_PENDING)
+ ->orWhere(function ($query) {
+ $query->where('sync_status', SubsiteOrder::SYNC_STATUS_FAILED)
+ ->where('retry_count', '<', 5)
+ ->where(function ($q) {
+ $q->whereNull('next_retry_at')
+ ->orWhere('next_retry_at', '<=', now());
+ });
+ })
+ ->with('order')
+ ->limit(50)
+ ->get();
+
+ $successCount = 0;
+ $failedCount = 0;
+
+ foreach ($pendingOrders as $subsiteOrder) {
+ try {
+ $result = $this->syncSingleOrder($subsite, $subsiteOrder);
+ if ($result) {
+ $subsiteOrder->markSyncSuccess();
+ $successCount++;
+ } else {
+ $subsiteOrder->markSyncFailed('同步失败');
+ $failedCount++;
+ }
+ } catch (\Exception $e) {
+ $subsiteOrder->markSyncFailed($e->getMessage());
+ $failedCount++;
+ Log::error('分站订单同步失败', [
+ 'subsite_id' => $subsite->id,
+ 'order_id' => $subsiteOrder->order_id,
+ 'error' => $e->getMessage()
+ ]);
+ }
+ }
+
+ // 更新最后同步时间
+ $subsite->updateLastSyncTime();
+
+ return [
+ 'success_count' => $successCount,
+ 'failed_count' => $failedCount,
+ 'total_count' => $pendingOrders->count()
+ ];
+ }
+
+ /**
+ * 同步单个订单
+ */
+ protected function syncSingleOrder(Subsite $subsite, SubsiteOrder $subsiteOrder): bool
+ {
+ $order = $subsiteOrder->order;
+
+ $syncData = [
+ 'order_sn' => $order->order_sn,
+ 'goods_name' => $order->goods->gd_name,
+ 'goods_price' => $order->actual_price,
+ 'quantity' => $order->buy_amount,
+ 'total_price' => $order->total_price,
+ 'email' => $order->email,
+ 'contact' => $order->contact ?? '',
+ 'status' => $order->status,
+ 'created_at' => $order->created_at->toISOString()
+ ];
+
+ // 如果有SKU信息,添加SKU数据
+ if ($order->goods_sku_id && $order->goodsSku) {
+ $syncData['sku_code'] = $order->goodsSku->sku_code;
+ $syncData['sku_name'] = $order->goodsSku->name;
+ $syncData['sku_attributes'] = $order->goodsSku->attributes;
+ }
+
+ $response = Http::timeout($subsite->getSetting('timeout', 30))
+ ->withHeaders([
+ 'Authorization' => 'Bearer ' . $subsite->api_key,
+ 'X-API-Secret' => $subsite->api_secret,
+ 'Content-Type' => 'application/json'
+ ])
+ ->post($subsite->api_url . '/api/orders/sync', $syncData);
+
+ if ($response->successful()) {
+ $responseData = $response->json();
+
+ // 保存分站返回的订单号
+ if (isset($responseData['order_sn'])) {
+ $subsiteOrder->update(['subsite_order_sn' => $responseData['order_sn']]);
+ }
+
+ // 保存同步数据
+ $subsiteOrder->setSyncData('response', $responseData);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 测试API连接
+ */
+ public function testApiConnection(Subsite $subsite): array
+ {
+ if (!$subsite->api_url) {
+ throw new \Exception('API地址未配置');
+ }
+
+ try {
+ $response = Http::timeout(10)
+ ->withHeaders([
+ 'Authorization' => 'Bearer ' . $subsite->api_key,
+ 'X-API-Secret' => $subsite->api_secret
+ ])
+ ->get($subsite->api_url . '/api/test');
+
+ if ($response->successful()) {
+ return [
+ 'status' => 'success',
+ 'message' => 'API连接正常',
+ 'response_time' => $response->transferStats->getTransferTime(),
+ 'data' => $response->json()
+ ];
+ } else {
+ return [
+ 'status' => 'error',
+ 'message' => 'API连接失败:HTTP ' . $response->status(),
+ 'response' => $response->body()
+ ];
+ }
+ } catch (\Exception $e) {
+ return [
+ 'status' => 'error',
+ 'message' => 'API连接异常:' . $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * 获取分站统计数据
+ */
+ public function getSubsiteStatistics(Subsite $subsite): array
+ {
+ $stats = [
+ 'total_orders' => $subsite->orders()->count(),
+ 'pending_orders' => $subsite->orders()
+ ->where('sync_status', SubsiteOrder::SYNC_STATUS_PENDING)
+ ->count(),
+ 'failed_orders' => $subsite->orders()
+ ->where('sync_status', SubsiteOrder::SYNC_STATUS_FAILED)
+ ->count(),
+ 'total_commission' => $subsite->orders()->sum('commission_amount'),
+ 'settled_commission' => $subsite->orders()
+ ->where('commission_status', SubsiteOrder::COMMISSION_STATUS_SETTLED)
+ ->sum('commission_amount'),
+ 'pending_commission' => $subsite->orders()
+ ->where('commission_status', SubsiteOrder::COMMISSION_STATUS_PENDING)
+ ->sum('commission_amount'),
+ 'balance' => $subsite->balance,
+ 'last_sync_at' => $subsite->last_sync_at
+ ];
+
+ // 今日统计
+ $today = now()->startOfDay();
+ $stats['today_orders'] = $subsite->orders()
+ ->where('created_at', '>=', $today)
+ ->count();
+ $stats['today_commission'] = $subsite->orders()
+ ->where('created_at', '>=', $today)
+ ->sum('commission_amount');
+
+ // 本月统计
+ $thisMonth = now()->startOfMonth();
+ $stats['month_orders'] = $subsite->orders()
+ ->where('created_at', '>=', $thisMonth)
+ ->count();
+ $stats['month_commission'] = $subsite->orders()
+ ->where('created_at', '>=', $thisMonth)
+ ->sum('commission_amount');
+
+ return $stats;
+ }
+
+ /**
+ * 创建分站订单记录
+ */
+ public function createSubsiteOrder(Order $order): void
+ {
+ // 获取所有启用的分站
+ $subsites = Subsite::where('status', Subsite::STATUS_ENABLED)->get();
+
+ foreach ($subsites as $subsite) {
+ // 计算佣金
+ $commissionAmount = $subsite->calculateCommission($order->total_price);
+
+ SubsiteOrder::create([
+ 'subsite_id' => $subsite->id,
+ 'order_id' => $order->id,
+ 'commission_amount' => $commissionAmount,
+ 'sync_data' => [
+ 'order_data' => $order->toArray(),
+ 'goods_data' => $order->goods->toArray(),
+ 'sku_data' => $order->goodsSku ? $order->goodsSku->toArray() : null
+ ]
+ ]);
+ }
+ }
+
+ /**
+ * 生成API密钥
+ */
+ protected function generateApiKey(): string
+ {
+ return 'sk_' . bin2hex(random_bytes(16));
+ }
+
+ /**
+ * 生成API秘钥
+ */
+ protected function generateApiSecret(): string
+ {
+ return bin2hex(random_bytes(32));
+ }
+}
diff --git a/composer.json b/composer.json
index c3de52a32..94ed233d9 100755
--- a/composer.json
+++ b/composer.json
@@ -8,28 +8,32 @@
],
"license": "MIT",
"require": {
- "php": "^7.2.5|^8.0",
- "amrshawky/laravel-currency": "^4.0",
- "dcat/easy-excel": "^1.0",
- "dcat/laravel-admin": "2.*",
- "fideloper/proxy": "^4.4",
+ "php": "^8.2",
+ "amrshawky/laravel-currency": "^6.0",
+ "dcat/easy-excel": "^2.0",
+ "dcat/laravel-admin": "3.*",
"germey/geetest": "^3.1",
"jenssegers/agent": "^2.6",
- "laravel/framework": "^6.20.26",
- "laravel/tinker": "^2.5",
- "mews/captcha": "^3.2",
+ "laravel/framework": "^10.0",
+ "laravel/sanctum": "^3.2",
+ "laravel/tinker": "^2.8",
+ "mews/captcha": "^3.3",
"paypal/rest-api-sdk-php": "^1.14",
- "simplesoftwareio/simple-qrcode": "2.0.0",
- "stripe/stripe-php": "^7.84",
+ "simplesoftwareio/simple-qrcode": "^4.2",
+ "stripe/stripe-php": "^10.0",
"xhat/payjs-laravel": "^1.6",
- "yansongda/pay": "^2.10"
+ "yansongda/pay": "^3.0",
+ "guzzlehttp/guzzle": "^7.2",
+ "predis/predis": "^2.0"
},
"require-dev": {
- "facade/ignition": "^1.16.15",
- "fakerphp/faker": "^1.9.1",
- "mockery/mockery": "^1.0",
- "nunomaduro/collision": "^3.0",
- "phpunit/phpunit": "^8.5.8|^9.3.3"
+ "fakerphp/faker": "^1.21",
+ "laravel/pint": "^1.0",
+ "laravel/sail": "^1.18",
+ "mockery/mockery": "^1.4.4",
+ "nunomaduro/collision": "^7.0",
+ "phpunit/phpunit": "^10.1",
+ "spatie/laravel-ignition": "^2.0"
},
"config": {
"optimize-autoloader": true,
@@ -44,12 +48,10 @@
},
"autoload": {
"psr-4": {
- "App\\": "app/"
+ "App\\": "app/",
+ "Database\\Factories\\": "database/factories/",
+ "Database\\Seeders\\": "database/seeders/"
},
- "classmap": [
- "database/seeds",
- "database/factories"
- ],
"files":[
"app/Helpers/functions.php"
]
diff --git a/database/migrations/2024_01_01_000001_create_subsites_table.php b/database/migrations/2024_01_01_000001_create_subsites_table.php
new file mode 100644
index 000000000..8c3496815
--- /dev/null
+++ b/database/migrations/2024_01_01_000001_create_subsites_table.php
@@ -0,0 +1,56 @@
+id()->comment('主键ID');
+ $table->string('name')->comment('分站名称');
+ $table->string('domain')->unique()->comment('分站域名');
+ $table->string('subdomain')->nullable()->unique()->comment('子域名');
+ $table->string('api_url')->nullable()->comment('API接口地址');
+ $table->string('api_key')->nullable()->comment('API密钥');
+ $table->string('api_secret')->nullable()->comment('API秘钥');
+ $table->tinyInteger('type')->default(1)->comment('分站类型:1=本站分站,2=第三方对接');
+ $table->tinyInteger('status')->default(1)->comment('状态:0=禁用,1=启用');
+ $table->decimal('commission_rate', 5, 2)->default(0)->comment('佣金比例(%)');
+ $table->decimal('balance', 10, 2)->default(0)->comment('分站余额');
+ $table->json('settings')->nullable()->comment('分站配置信息');
+ $table->json('api_config')->nullable()->comment('API配置参数');
+ $table->text('description')->nullable()->comment('分站描述');
+ $table->string('contact_email')->nullable()->comment('联系邮箱');
+ $table->string('contact_phone')->nullable()->comment('联系电话');
+ $table->string('logo_url')->nullable()->comment('分站Logo地址');
+ $table->string('theme_color')->nullable()->comment('主题颜色');
+ $table->timestamp('last_sync_at')->nullable()->comment('最后同步时间');
+ $table->timestamps();
+ $table->softDeletes();
+
+ // 索引优化
+ $table->index(['status', 'type'], 'idx_status_type');
+ $table->index('domain', 'idx_domain');
+ $table->index('subdomain', 'idx_subdomain');
+ $table->index('last_sync_at', 'idx_last_sync');
+ });
+ }
+
+ /**
+ * 回滚迁移
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('subsites');
+ }
+};
diff --git a/database/migrations/2024_01_01_000002_create_subsite_orders_table.php b/database/migrations/2024_01_01_000002_create_subsite_orders_table.php
new file mode 100644
index 000000000..b5ef4d347
--- /dev/null
+++ b/database/migrations/2024_01_01_000002_create_subsite_orders_table.php
@@ -0,0 +1,55 @@
+id()->comment('主键ID');
+ $table->unsignedBigInteger('subsite_id')->comment('分站ID');
+ $table->unsignedBigInteger('order_id')->comment('订单ID');
+ $table->string('subsite_order_sn')->nullable()->comment('分站订单号');
+ $table->decimal('commission_amount', 10, 2)->default(0)->comment('佣金金额');
+ $table->tinyInteger('commission_status')->default(0)->comment('佣金状态:0=未结算,1=已结算');
+ $table->timestamp('commission_settled_at')->nullable()->comment('佣金结算时间');
+ $table->json('sync_data')->nullable()->comment('同步数据');
+ $table->tinyInteger('sync_status')->default(0)->comment('同步状态:0=未同步,1=已同步,2=同步失败');
+ $table->timestamp('synced_at')->nullable()->comment('同步时间');
+ $table->text('sync_error')->nullable()->comment('同步错误信息');
+ $table->integer('retry_count')->default(0)->comment('重试次数');
+ $table->timestamp('next_retry_at')->nullable()->comment('下次重试时间');
+ $table->timestamps();
+
+ // 外键约束
+ $table->foreign('subsite_id')->references('id')->on('subsites')->onDelete('cascade');
+ $table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
+
+ // 索引优化
+ $table->index(['subsite_id', 'commission_status'], 'idx_subsite_commission');
+ $table->index(['order_id', 'sync_status'], 'idx_order_sync');
+ $table->index('commission_settled_at', 'idx_commission_settled');
+ $table->index('synced_at', 'idx_synced');
+ $table->index('next_retry_at', 'idx_next_retry');
+ $table->unique(['subsite_id', 'order_id'], 'uk_subsite_order');
+ });
+ }
+
+ /**
+ * 回滚迁移
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('subsite_orders');
+ }
+};
diff --git a/database/migrations/2024_01_01_000003_create_shopping_carts_table.php b/database/migrations/2024_01_01_000003_create_shopping_carts_table.php
new file mode 100644
index 000000000..00bef1aa6
--- /dev/null
+++ b/database/migrations/2024_01_01_000003_create_shopping_carts_table.php
@@ -0,0 +1,53 @@
+id()->comment('主键ID');
+ $table->string('session_id')->comment('会话ID');
+ $table->string('user_email')->nullable()->comment('用户邮箱');
+ $table->unsignedBigInteger('goods_id')->comment('商品ID');
+ $table->unsignedBigInteger('goods_sku_id')->nullable()->comment('商品规格ID');
+ $table->integer('quantity')->default(1)->comment('购买数量');
+ $table->decimal('price', 10, 2)->comment('商品单价');
+ $table->decimal('total_price', 10, 2)->comment('商品总价');
+ $table->json('goods_snapshot')->nullable()->comment('商品信息快照');
+ $table->json('sku_snapshot')->nullable()->comment('规格信息快照');
+ $table->json('custom_fields')->nullable()->comment('自定义字段数据');
+ $table->string('coupon_code')->nullable()->comment('优惠券代码');
+ $table->decimal('discount_amount', 10, 2)->default(0)->comment('优惠金额');
+ $table->timestamp('expires_at')->nullable()->comment('过期时间');
+ $table->timestamps();
+
+ // 外键约束
+ $table->foreign('goods_id')->references('id')->on('goods')->onDelete('cascade');
+
+ // 索引优化
+ $table->index(['session_id', 'user_email'], 'idx_session_email');
+ $table->index(['goods_id', 'goods_sku_id'], 'idx_goods_sku');
+ $table->index('expires_at', 'idx_expires');
+ $table->index('created_at', 'idx_created');
+ });
+ }
+
+ /**
+ * 回滚迁移
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('shopping_carts');
+ }
+};
diff --git a/database/migrations/2024_01_01_000004_create_goods_skus_table.php b/database/migrations/2024_01_01_000004_create_goods_skus_table.php
new file mode 100644
index 000000000..d65048567
--- /dev/null
+++ b/database/migrations/2024_01_01_000004_create_goods_skus_table.php
@@ -0,0 +1,59 @@
+id()->comment('主键ID');
+ $table->unsignedBigInteger('goods_id')->comment('商品ID');
+ $table->string('sku_code')->unique()->comment('SKU编码');
+ $table->string('name')->comment('规格名称');
+ $table->json('attributes')->comment('规格属性JSON');
+ $table->decimal('price', 10, 2)->comment('规格价格');
+ $table->decimal('wholesale_price', 10, 2)->nullable()->comment('批发价格');
+ $table->decimal('cost_price', 10, 2)->nullable()->comment('成本价格');
+ $table->integer('stock')->default(0)->comment('库存数量');
+ $table->integer('sold_count')->default(0)->comment('销售数量');
+ $table->integer('warning_stock')->default(10)->comment('库存预警数量');
+ $table->tinyInteger('status')->default(1)->comment('状态:0=禁用,1=启用');
+ $table->string('image')->nullable()->comment('规格图片');
+ $table->decimal('weight', 8, 2)->nullable()->comment('重量(kg)');
+ $table->string('barcode')->nullable()->comment('条形码');
+ $table->string('supplier_code')->nullable()->comment('供应商编码');
+ $table->integer('sort')->default(0)->comment('排序权重');
+ $table->json('extra_data')->nullable()->comment('扩展数据');
+ $table->timestamps();
+ $table->softDeletes();
+
+ // 外键约束
+ $table->foreign('goods_id')->references('id')->on('goods')->onDelete('cascade');
+
+ // 索引优化
+ $table->index(['goods_id', 'status'], 'idx_goods_status');
+ $table->index(['sku_code', 'status'], 'idx_sku_status');
+ $table->index(['stock', 'status'], 'idx_stock_status');
+ $table->index('warning_stock', 'idx_warning_stock');
+ $table->index('sort', 'idx_sort');
+ });
+ }
+
+ /**
+ * 回滚迁移
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('goods_skus');
+ }
+};
diff --git a/database/migrations/2024_01_01_000005_create_goods_attributes_table.php b/database/migrations/2024_01_01_000005_create_goods_attributes_table.php
new file mode 100644
index 000000000..f94ed24d8
--- /dev/null
+++ b/database/migrations/2024_01_01_000005_create_goods_attributes_table.php
@@ -0,0 +1,53 @@
+id()->comment('主键ID');
+ $table->unsignedBigInteger('goods_id')->comment('商品ID');
+ $table->string('name')->comment('属性名称');
+ $table->json('values')->comment('属性值列表');
+ $table->tinyInteger('type')->default(1)->comment('属性类型:1=文本,2=颜色,3=图片,4=尺寸,5=数字');
+ $table->tinyInteger('input_type')->default(1)->comment('输入类型:1=单选,2=多选,3=输入框,4=下拉框');
+ $table->integer('sort')->default(0)->comment('排序权重');
+ $table->tinyInteger('is_required')->default(1)->comment('是否必选:0=否,1=是');
+ $table->tinyInteger('is_filterable')->default(0)->comment('是否可筛选:0=否,1=是');
+ $table->tinyInteger('is_searchable')->default(0)->comment('是否可搜索:0=否,1=是');
+ $table->string('unit')->nullable()->comment('属性单位');
+ $table->string('default_value')->nullable()->comment('默认值');
+ $table->text('description')->nullable()->comment('属性描述');
+ $table->json('validation_rules')->nullable()->comment('验证规则');
+ $table->timestamps();
+
+ // 外键约束
+ $table->foreign('goods_id')->references('id')->on('goods')->onDelete('cascade');
+
+ // 索引优化
+ $table->index(['goods_id', 'sort'], 'idx_goods_sort');
+ $table->index(['goods_id', 'is_filterable'], 'idx_goods_filterable');
+ $table->index(['goods_id', 'is_searchable'], 'idx_goods_searchable');
+ $table->index(['type', 'input_type'], 'idx_type_input');
+ });
+ }
+
+ /**
+ * 回滚迁移
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('goods_attributes');
+ }
+};
diff --git a/database/migrations/2024_01_01_000006_add_sku_support_to_existing_tables.php b/database/migrations/2024_01_01_000006_add_sku_support_to_existing_tables.php
new file mode 100644
index 000000000..dfaaf9643
--- /dev/null
+++ b/database/migrations/2024_01_01_000006_add_sku_support_to_existing_tables.php
@@ -0,0 +1,64 @@
+unsignedBigInteger('goods_sku_id')->nullable()->after('goods_id')->comment('商品规格ID');
+ $table->json('sku_snapshot')->nullable()->after('info')->comment('规格信息快照');
+ $table->foreign('goods_sku_id')->references('id')->on('goods_skus')->onDelete('set null');
+ $table->index('goods_sku_id', 'idx_goods_sku_id');
+ });
+
+ // 为卡密表添加SKU支持
+ Schema::table('carmis', function (Blueprint $table) {
+ $table->unsignedBigInteger('goods_sku_id')->nullable()->after('goods_id')->comment('商品规格ID');
+ $table->foreign('goods_sku_id')->references('id')->on('goods_skus')->onDelete('set null');
+ $table->index(['goods_id', 'goods_sku_id', 'status'], 'idx_goods_sku_status');
+ });
+
+ // 为商品表添加多规格支持标识
+ Schema::table('goods', function (Blueprint $table) {
+ $table->tinyInteger('has_sku')->default(0)->after('type')->comment('是否有多规格:0=否,1=是');
+ $table->decimal('min_price', 10, 2)->nullable()->after('actual_price')->comment('最低价格');
+ $table->decimal('max_price', 10, 2)->nullable()->after('min_price')->comment('最高价格');
+ $table->index('has_sku', 'idx_has_sku');
+ });
+ }
+
+ /**
+ * 回滚迁移
+ */
+ public function down(): void
+ {
+ Schema::table('orders', function (Blueprint $table) {
+ $table->dropForeign(['goods_sku_id']);
+ $table->dropIndex('idx_goods_sku_id');
+ $table->dropColumn(['goods_sku_id', 'sku_snapshot']);
+ });
+
+ Schema::table('carmis', function (Blueprint $table) {
+ $table->dropForeign(['goods_sku_id']);
+ $table->dropIndex('idx_goods_sku_status');
+ $table->dropColumn('goods_sku_id');
+ });
+
+ Schema::table('goods', function (Blueprint $table) {
+ $table->dropIndex('idx_has_sku');
+ $table->dropColumn(['has_sku', 'min_price', 'max_price']);
+ });
+ }
+};
diff --git a/routes/common/web.php b/routes/common/web.php
index c112fb00c..095d0d08c 100644
--- a/routes/common/web.php
+++ b/routes/common/web.php
@@ -32,6 +32,32 @@
Route::post('search-order-by-email', 'OrderController@searchOrderByEmail');
// 通过浏览器查询
Route::post('search-order-by-browser', 'OrderController@searchOrderByBrowser');
+
+ // 购物车路由
+ Route::prefix('cart')->group(function () {
+ Route::get('/', 'ShoppingCartController@index')->name('cart.index');
+ Route::post('/add', 'ShoppingCartController@add')->name('cart.add');
+ Route::put('/update/{cartItem}', 'ShoppingCartController@update')->name('cart.update');
+ Route::delete('/remove/{cartItem}', 'ShoppingCartController@remove')->name('cart.remove');
+ Route::delete('/clear', 'ShoppingCartController@clear')->name('cart.clear');
+ Route::post('/apply-coupon', 'ShoppingCartController@applyCoupon')->name('cart.apply-coupon');
+ Route::delete('/remove-coupon/{cartItem}', 'ShoppingCartController@removeCoupon')->name('cart.remove-coupon');
+ Route::get('/total', 'ShoppingCartController@getTotal')->name('cart.total');
+ Route::post('/checkout', 'ShoppingCartController@checkout')->name('cart.checkout');
+ });
+
+ // 商品规格API
+ Route::get('/api/goods/{goods}/skus', function (\App\Models\Goods $goods) {
+ return $goods->getAvailableSkus();
+ });
+});
+
+// 分站API路由(不需要中间件)
+Route::prefix('api/subsite')->namespace('Api')->group(function () {
+ Route::post('/orders/sync', 'SubsiteController@syncOrder');
+ Route::get('/test', 'SubsiteController@test');
+ Route::get('/goods', 'SubsiteController@getGoods');
+ Route::get('/order-status', 'SubsiteController@getOrderStatus');
});
Route::group(['middleware' => ['install.check'],'namespace' => 'Home'], function () {