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 '' . $this->name . ''; + 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 () {