diff --git a/.env.example b/.env.example
index e1f3dcad..8a4f3623 100644
--- a/.env.example
+++ b/.env.example
@@ -7,7 +7,7 @@ APP_URL=http://localhost
ENDPOINT_API_DOMAIN=null
ENDPOINT_API_PATH=null
-LOG_CHANNEL=stack
+LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
@@ -20,7 +20,7 @@ DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
-FILESYSTEM_DRIVER=local
+FILESYSTEM_DRIVER=public
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
diff --git a/README.md b/README.md
index 421dca2a..6efacf4a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
## Require
-- php >= 8.0
+- php >= 8.1
## Step
diff --git a/app/Admin/Actions/Form/ProductImportForm.php b/app/Admin/Actions/Form/ProductImportForm.php
index 047e6c49..90c378aa 100644
--- a/app/Admin/Actions/Form/ProductImportForm.php
+++ b/app/Admin/Actions/Form/ProductImportForm.php
@@ -4,7 +4,7 @@ namespace App\Admin\Actions\Form;
use Dcat\Admin\Widgets\Form;
use Illuminate\Support\Facades\DB;
-use App\Admin\Imports\Product;
+use App\Admin\Imports\{Product, StoreProduct};
class ProductImportForm extends Form
{
@@ -13,12 +13,15 @@ class ProductImportForm extends Form
try {
DB::beginTransaction();
$file = $input['file'];
- (new Product())->load($file);
+ if (data_get($input, 'is_store')) {
+ (new StoreProduct())->load($file);
+ } else {
+ (new Product())->load($file);
+ }
DB::commit();
return $this->response()->success('数据导入成功')->refresh();
} catch (\Exception $e) {
DB::rollback();
- dd($e);
return $this->response()->error($e->getMessage());
}
}
@@ -29,5 +32,6 @@ class ProductImportForm extends Form
->attribute('type', 'file')
->attribute('accept', 'application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->rules('required', ['required' => '文件不能为空']);
+ $this->hidden('is_store');
}
}
diff --git a/app/Admin/Actions/Store/DownloadProductTemplate.php b/app/Admin/Actions/Store/DownloadProductTemplate.php
new file mode 100644
index 00000000..54dfa7b2
--- /dev/null
+++ b/app/Admin/Actions/Store/DownloadProductTemplate.php
@@ -0,0 +1,19 @@
+下载商品模板
+ HTML;
+ }
+}
diff --git a/app/Admin/Actions/Store/ImportProduct.php b/app/Admin/Actions/Store/ImportProduct.php
new file mode 100644
index 00000000..67bed180
--- /dev/null
+++ b/app/Admin/Actions/Store/ImportProduct.php
@@ -0,0 +1,21 @@
+ 1]);
+ return Modal::make()
+ ->lg()
+ ->title($this->title)
+ ->body($form)
+ ->button($this->html());
+ }
+}
\ No newline at end of file
diff --git a/app/Admin/Controllers/ProductCategoryController.php b/app/Admin/Controllers/ProductCategoryController.php
index 6daf670a..31dc6769 100644
--- a/app/Admin/Controllers/ProductCategoryController.php
+++ b/app/Admin/Controllers/ProductCategoryController.php
@@ -111,6 +111,7 @@ class ProductCategoryController extends AdminController
$form->select('parent_id')->options(ProductCategoryModel::selectOptions());
$form->text('name')->required();
$form->image('icon')
+ ->uniqueName()
->move('product-categories/'.Carbon::now()->toDateString())
->saveFullUrl()
->removable(false)
diff --git a/app/Admin/Controllers/Store/ProductController.php b/app/Admin/Controllers/Store/ProductController.php
index 7aa34a34..101024e6 100644
--- a/app/Admin/Controllers/Store/ProductController.php
+++ b/app/Admin/Controllers/Store/ProductController.php
@@ -74,6 +74,11 @@ class ProductController extends AdminController
$filter->equal('productSku.category_id', '分类')->select(admin_route('api.product_categories', ['level' => 2]))->width(3);
$filter->like('productSku.name', '名称')->width(3);
});
+ $grid->tools(function (Grid\Tools $tools) {
+ $tools->append(new \App\Admin\Actions\Store\ImportProduct());
+ $tools->append(new \App\Admin\Actions\Store\DownloadProductTemplate());
+ });
+
$grid->footer(function ($collection) use ($grid) {
$query = StoreProductSku::join('product_skus', 'product_skus.id', '=', 'store_product_skus.product_sku_id');
$grid->model()->getQueries()->unique()->each(function ($value) use (&$query) {
diff --git a/app/Admin/Controllers/Store/StoreController.php b/app/Admin/Controllers/Store/StoreController.php
index 1aa40a83..a9cbcb5a 100644
--- a/app/Admin/Controllers/Store/StoreController.php
+++ b/app/Admin/Controllers/Store/StoreController.php
@@ -81,13 +81,6 @@ class StoreController extends AdminController
}
- /**
- * Make a show builder.
- *
- * @param mixed $id
- *
- * @return Show
- */
protected function detail($id)
{
if ($this->canAdmin()) {
@@ -107,6 +100,10 @@ class StoreController extends AdminController
$show->tools(function (Tools $tools) {
$tools->append(new ExportGoodsSpu());
+
+ $tools->disableList();
+ $tools->disableDelete();
+ $tools->disableEdit();
});
return $show;
}
diff --git a/app/Admin/Imports/Product.php b/app/Admin/Imports/Product.php
index e13d2d84..b216d4e6 100644
--- a/app/Admin/Imports/Product.php
+++ b/app/Admin/Imports/Product.php
@@ -21,21 +21,43 @@ class Product
continue;
}
$cells = $row->toArray();
+
+ $index = 0;
+ $goodsName = $cells[$index];// 商品名称
+ $goodsTitle = $cells[++$index];// 商品副标题
+ $categoryName = $cells[++$index];// 分类名称
+ $goodsImage = $cells[++$index];// 商品图片
+ $salePrice = $cells[++$index];// 销售价
+ $marketPrice = $cells[++$index];// 市场价
+ $costPrice = $cells[++$index];// 成本价
+ $vipPrice = $cells[++$index];// 会员价
+ $saleValue = $cells[++$index];// 销售值
+ // 属性加价
+ // 颜色[远峰蓝:0,石墨色:0,银色:0,金色:0,苍岭绿色:0]
+ $rowAttrs = $cells[++$index];
+ // 规格展示
+ // 主体[上市月份:9月,品牌:Apple]
+ $rowSpecs = $cells[++$index];
+ $shipTemplate = $cells[++$index];// 运费模板
+ $goodsWeight = $cells[++$index];// 商品重量(克)
+ // 详细描述
+ // https://qiniu.abcdefg.fun/banner/banner4.png,https://qiniu.abcdefg.fun/banner/banner5.png
+ $goodsContent = $cells[++$index];
$goods = [
- 'name' => $cells[0],
- 'subtitle' => $cells[1],
+ 'name' => $goodsName,
+ 'subtitle' => $goodsTitle,
'shipping_template_id' => 1,
];
- $category = ProductCategory::where('name', $cells[2])->firstOrFail();
+ $category = ProductCategory::where('name', $categoryName)->firstOrFail();
$goods['category_id'] = $category->id;
// 图片组
- if ($path = $cells[3]) {
- if (Str::startsWith($path, ['http://', 'https://'])) {
- $images = explode(',', $path);
+ if ($goodsImage) {
+ if (Str::startsWith($goodsImage, ['http://', 'https://'])) {
+ $images = explode(',', $goodsImage);
} else {
- $images = $this->getImageUrlFromPath($path);
+ $images = $this->getImageUrlFromPath($goodsImage);
}
if (count($images) > 0) {
$goods['cover'] = $images[0];
@@ -44,39 +66,37 @@ class Product
}
// 运费模板
- if ($cell_11 = data_get($cells, 11)) {
- $goods['shipping_template_id'] = $cell_11;
+ if ($shipTemplate) {
+ $goods['shipping_template_id'] = $shipTemplate;
}
// 重量
- if ($cell_12 = data_get($cells, 12)) {
- $goods['weight'] = $cell_12;
+ if ($goodsWeight) {
+ $goods['weight'] = $goodsWeight;
}
// 详细描述
- if ($cell_13 = data_get($cells, 13)) {
+ if ($goodsContent) {
$description = '';
- foreach(explode(',', $cell_13) as $item) {
+ foreach(explode(',', $goodsContent) as $item) {
$description .= '

';
}
$goods['description'] = $description;
}
$goods = array_merge($goods, [
- 'sell_price' => $cells[4] * 100,
- 'market_price' => $cells[5] * 100,
- 'cost_price' => $cells[6] * 100,
- 'vip_price' => $cells[7] * 100,
- 'sales_value' => $cells[8],
+ 'sell_price' => $salePrice * 100,
+ 'market_price' => $marketPrice * 100,
+ 'cost_price' => $costPrice * 100,
+ 'vip_price' => $vipPrice * 100,
+ 'sales_value' => $saleValue,
]);
$product = ProductSpu::updateOrCreate(['name' => $goods['name']],$goods);
// 属性规格
- $coll_9 = data_get($cells, 9);
- $cell_10 = data_get($cells, 10);
- if ($coll_9) {
- $format_specs = $this->formatAttr($coll_9);
- $format_attrs = $this->formatAttr($cell_10);
- $spec_group = $this->getSpec($cells[0], $coll_9, $cell_10);
+ if ($rowAttrs) {
+ $format_specs = $this->formatAttr($rowAttrs);
+ $format_attrs = $this->formatAttr($rowSpecs);
+ $spec_group = $this->getSpec($cells[0], $rowAttrs, $rowSpecs);
$attrs = [];
foreach($spec_group->attrs as $index => $item) {
diff --git a/app/Admin/Imports/StoreProduct.php b/app/Admin/Imports/StoreProduct.php
new file mode 100644
index 00000000..6d5a1028
--- /dev/null
+++ b/app/Admin/Imports/StoreProduct.php
@@ -0,0 +1,219 @@
+open($file);
+
+ foreach ($reader->getSheetIterator() as $sheet) {
+ foreach ($sheet->getRowIterator() as $num => $row) {
+ if ($num === 1) {
+ continue;
+ }
+ $cells = $row->toArray();
+
+ $index = 0;
+ $storeName = $cells[$index];// 店铺名称
+ $goodsName = $cells[++$index];// 商品名称
+ $goodsTitle = $cells[++$index];// 商品副标题
+ $categoryName = $cells[++$index];// 分类名称
+ $goodsImage = $cells[++$index];// 商品图片
+ $salePrice = $cells[++$index];// 销售价
+ $marketPrice = $cells[++$index];// 市场价
+ $costPrice = $cells[++$index];// 成本价
+ $vipPrice = $cells[++$index];// 会员价
+ $saleValue = $cells[++$index];// 销售值
+ // 属性加价
+ // 颜色[远峰蓝:0,石墨色:0,银色:0,金色:0,苍岭绿色:0]
+ $rowAttrs = $cells[++$index];
+ // 规格展示
+ // 主体[上市月份:9月,品牌:Apple]
+ $rowSpecs = $cells[++$index];
+ $shipTemplate = $cells[++$index];// 运费模板
+ $goodsWeight = $cells[++$index];// 商品重量(克)
+ // 详细描述
+ // https://qiniu.abcdefg.fun/banner/banner4.png,https://qiniu.abcdefg.fun/banner/banner5.png
+ $goodsContent = $cells[++$index];
+
+ $stock = $cells[++$index];// 库存
+
+ $store = Store::where(['title' => $storeName])->firstOrFail();
+
+ $goods = [
+ 'name' => $goodsName,
+ 'subtitle' => $goodsTitle,
+ 'shipping_template_id' => 1,
+ ];
+ $category = ProductCategory::where('name', $categoryName)->firstOrFail();
+ $goods['category_id'] = $category->id;
+
+ // 图片组
+ if ($goodsImage) {
+ if (Str::startsWith($goodsImage, ['http://', 'https://'])) {
+ $images = explode(',', $goodsImage);
+ } else {
+ $images = $this->getImageUrlFromPath($goodsImage);
+ }
+ if (count($images) > 0) {
+ $goods['cover'] = $images[0];
+ $goods['images'] = $images;
+ }
+ }
+
+ // 运费模板
+ if ($shipTemplate) {
+ $goods['shipping_template_id'] = $shipTemplate;
+ }
+ // 重量
+ if ($goodsWeight) {
+ $goods['weight'] = $goodsWeight;
+ }
+ // 详细描述
+ if ($goodsContent) {
+ $description = '';
+ foreach(explode(',', $goodsContent) as $item) {
+ $description .= '
';
+ }
+ $goods['description'] = $description;
+ }
+
+ $goods = array_merge($goods, [
+ 'sell_price' => $salePrice * 100,
+ 'market_price' => $marketPrice * 100,
+ 'cost_price' => $costPrice * 100,
+ 'vip_price' => $vipPrice * 100,
+ 'sales_value' => $saleValue,
+ 'stock' => $stock,
+ ]);
+
+ $product = ProductSpu::updateOrCreate(['name' => $goods['name']],$goods);
+
+ // 属性规格
+ if ($rowAttrs) {
+ $format_specs = $this->formatAttr($rowAttrs);
+ $format_attrs = $this->formatAttr($rowSpecs);
+ $spec_group = $this->getSpec($cells[0], $rowAttrs, $rowSpecs);
+
+ $attrs = [];
+ foreach($spec_group->attrs as $index => $item) {
+ $attr = ['name' => $item['title'], 'attrs' => []];
+ foreach(explode(PHP_EOL, $item['value']) as $value) {
+ array_push($attr['attrs'], ['name' => $value, 'value' => data_get($format_attrs, "$index.values.$value")]);
+ }
+ array_push($attrs, $attr);
+ }
+ $product->update(['attrs' => $attrs]);
+
+ $specs = [];
+ $product->specs()->delete();
+ foreach($spec_group->specs as $index => $item) {
+ $spec = ['name' => $item['title'], 'items' => []];
+ foreach(explode(PHP_EOL, $item['value']) as $value) {
+ array_push($spec['items'], ['name' => $value, 'value' => data_get($format_specs, "$index.values.$value")]);
+ }
+ array_push($specs, $spec);
+ }
+ $product->specs()->createMany($specs);
+ }
+
+ // 清空现有的sku, 重新添加
+ StoreProductSku::where('store_id', $store->id)->whereIn('product_sku_id', $product->skus->pluck('id'))->delete();
+ $product->skus()->delete();
+ ProductSku::createBySpu($product);
+
+ // 将sku 添加到门店
+ $skuList = $product->skus()->get();
+ $skus = [];
+ foreach($skuList as $sku) {
+ array_push($skus, ['amount' => $stock, 'product_sku_id' => $sku->id, 'status' => 1, 'store_id' => $store->id]);
+ }
+ StoreProductSku::insert($skus);
+ }
+ }
+
+ $reader->close();
+ }
+
+ protected function getImageUrlFromPath($dir)
+ {
+ $disk = Storage::disk();
+ $files = $disk->files($dir);
+ $images = [];
+
+ foreach($files as $filename) {
+ array_push($images, $disk->url($filename));
+ }
+
+ return $images;
+ }
+
+ protected function getSpec($name, $specs, $attrs)
+ {
+ if (Str::contains($specs, ':')) {
+ // 创建新的规格
+ $spec = ProductGroup::updateOrCreate([
+ 'name' => $name
+ ], [
+ 'attrs' => $this->formatGroupAttr($this->formatAttr($attrs)),
+ 'specs' => $this->formatGroupAttr($this->formatAttr($specs))
+ ]);
+ } else {
+ $spec = ProductGroup::where('name', $specs)->firstOrFail();
+ }
+
+ return $spec;
+ }
+
+ /**
+ * 颜色[灰色:0,银色:0]
+ * 尺寸[13寸:0,14寸:500,16寸:1000]
+ * 处理器[M1:0,M1 Pro:1000,M1 Max:1500]
+ * 内存[8G:0,16G:1000,32G:2000]
+ * 硬盘[256G:0,512G:1000,1TB:2000]
+ *
+ * [
+ * ['name' => '颜色', 'values' => ['灰色' => 0, '银色' => 0]],
+ * ['name' => '尺寸', 'values' => ['13寸' => 0, '14寸' => 500, '16寸' => '1000']]
+ * ...
+ * ]
+ */
+ protected function formatAttr($attrs)
+ {
+ $attr_data = [];
+ if (!Str::contains($attrs, ':')) {
+ return $attr_data;
+ }
+ foreach(explode("\n", $attrs) as $item) {
+ $name = explode('[', $item)[0];
+ $attr_values = [];
+ foreach(explode(',', Str::between($item, '[', ']')) as $item1) {
+ $item1_explode = explode(':', $item1);
+ $attr_values[$item1_explode[0]] = $item1_explode[1];
+ }
+ array_push($attr_data, ['name' => $name, 'values' => $attr_values]);
+ }
+
+ return $attr_data;
+ }
+
+ protected function formatGroupAttr($data)
+ {
+ $list = [];
+ foreach($data as $item) {
+ array_push($list, ['title' => $item['name'], 'value' => implode(PHP_EOL, array_keys($item['values']))]);
+ }
+ return $list;
+ }
+}
diff --git a/app/Endpoint/Api/Http/Controllers/Auth/LoginController.php b/app/Endpoint/Api/Http/Controllers/Auth/LoginController.php
index 169fab8f..bbcc9570 100644
--- a/app/Endpoint/Api/Http/Controllers/Auth/LoginController.php
+++ b/app/Endpoint/Api/Http/Controllers/Auth/LoginController.php
@@ -72,7 +72,7 @@ class LoginController extends Controller
]);
$user = User::where('phone', $validated['phone'])->first();
-
+
if (! $user?->verifyPassword($validated['password'])) {
throw new BizException(__('Incorrect account or password'));
}
diff --git a/app/Endpoint/Api/Http/Controllers/StoreController.php b/app/Endpoint/Api/Http/Controllers/StoreController.php
index 7cb6aca8..06c3c621 100644
--- a/app/Endpoint/Api/Http/Controllers/StoreController.php
+++ b/app/Endpoint/Api/Http/Controllers/StoreController.php
@@ -44,7 +44,7 @@ class StoreController extends Controller
$skus = $store->productSkus()
->select($fields)
->filter($input)
- ->online()
+ // ->online()
->wherePivot('status', 1)
->simplePaginate(Paginator::resolvePerPage('per_page', 20, 50));
diff --git a/app/Models/Store/Store.php b/app/Models/Store/Store.php
index bbc8b916..4ad63770 100644
--- a/app/Models/Store/Store.php
+++ b/app/Models/Store/Store.php
@@ -10,6 +10,8 @@ class Store extends Model
{
use HasFactory, HasDateTimeFormatter;
+ protected $fillable = ['title', 'image'];
+
protected $attributes = [
'status' => 1,
'sort' => 1
diff --git a/app/Models/User.php b/app/Models/User.php
index 83f8becf..0ce8a557 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -353,6 +353,7 @@ class User extends Model implements AuthorizableContract, AuthenticatableContrac
*/
public function verifyPassword(string $password): bool
{
+ logger('password', ['p1' => $password, 'p2' => $this->password]);
// 如果旧密码存在,则校验旧密码
if ($this->old_password) {
return $this->old_password === md5($password);
diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php
index f8218ef4..903f76c1 100644
--- a/app/Providers/RouteServiceProvider.php
+++ b/app/Providers/RouteServiceProvider.php
@@ -38,6 +38,7 @@ class RouteServiceProvider extends ServiceProvider
$this->configureRateLimiting();
$this->routes(function () {
+ Route::middleware('web')->group(base_path('routes/web.php'));
Route::domain(config('endpoint.api.domain'))
->group(function () {
Route::prefix(config('endpoint.api.path'))
diff --git a/public/store-goods.xlsx b/public/store-goods.xlsx
new file mode 100644
index 00000000..d2f90176
Binary files /dev/null and b/public/store-goods.xlsx differ
diff --git a/routes/web.php b/routes/web.php
index b8d4384e..fd646792 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -2,9 +2,4 @@
use Illuminate\Support\Facades\Route;
-Route::fallback(function () {
- return response()->json([
- 'errcode' => 404,
- 'message' => 'Not Found',
- ], 404);
-});
+Route::redirect('', '/admin');