添加市场
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -20,7 +20,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
|||||||
class ProductExport implements FromCollection, WithStrictNullComparison, WithStyles
|
class ProductExport implements FromCollection, WithStrictNullComparison, WithStyles
|
||||||
{
|
{
|
||||||
/** 列头(与 ProductImport 的解析顺序一一对应,改动需同步) */
|
/** 列头(与 ProductImport 的解析顺序一一对应,改动需同步) */
|
||||||
public const array HEADERS = ['品名', '分类', '供应商', '规格/包规', '单位', '成本价', '排序', '库存', '保质期', '状态', '备注'];
|
public const array HEADERS = ['品名', '分类', '供应商', '市场', '规格/包规', '单位', '成本价', '排序', '库存', '保质期', '状态', '备注'];
|
||||||
|
|
||||||
private ?Collection $rows = null;
|
private ?Collection $rows = null;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ class ProductExport implements FromCollection, WithStrictNullComparison, WithSty
|
|||||||
$rows = [self::HEADERS];
|
$rows = [self::HEADERS];
|
||||||
|
|
||||||
if ($this->template) {
|
if ($this->template) {
|
||||||
$rows[] = ['西红柿', '蔬菜/茄果类', '示例供应商', '10斤/箱', '斤', 2.5, 0, 100, 3, '上架', '示例行,导入前请删除'];
|
$rows[] = ['西红柿', '蔬菜/茄果类', '示例供应商', '新发地', '10斤/箱', '斤', 2.5, 0, 100, 3, '上架', '示例行,导入前请删除'];
|
||||||
return $this->rows = collect($rows);
|
return $this->rows = collect($rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ class ProductExport implements FromCollection, WithStrictNullComparison, WithSty
|
|||||||
$product->name,
|
$product->name,
|
||||||
$categoryPaths[(int) $product->category_id] ?? '',
|
$categoryPaths[(int) $product->category_id] ?? '',
|
||||||
$supplierNames[(int) $product->supplier_id] ?? '',
|
$supplierNames[(int) $product->supplier_id] ?? '',
|
||||||
|
$product->market,
|
||||||
$product->spec,
|
$product->spec,
|
||||||
$product->unit,
|
$product->unit,
|
||||||
(float) $product->cost_price,
|
(float) $product->cost_price,
|
||||||
@@ -87,7 +88,7 @@ class ProductExport implements FromCollection, WithStrictNullComparison, WithSty
|
|||||||
$this->collection();
|
$this->collection();
|
||||||
|
|
||||||
$sheet->freezePane('A2');
|
$sheet->freezePane('A2');
|
||||||
$widths = [20, 16, 14, 14, 8, 10, 8, 8, 8, 8, 24];
|
$widths = [20, 16, 14, 12, 14, 8, 10, 8, 8, 8, 8, 24];
|
||||||
foreach ($widths as $index => $width) {
|
foreach ($widths as $index => $width) {
|
||||||
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
|
|||||||
'category' => $categoryNames[(int) $productId] ?? '',
|
'category' => $categoryNames[(int) $productId] ?? '',
|
||||||
'product_name' => (string) ($snapshot->product_name ?? $product->name),
|
'product_name' => (string) ($snapshot->product_name ?? $product->name),
|
||||||
'supplier' => $supplierNames[$rowSupplierId] ?? '',
|
'supplier' => $supplierNames[$rowSupplierId] ?? '',
|
||||||
|
'market' => (string) ($product->market ?? ''),
|
||||||
'product_spec' => $spec,
|
'product_spec' => $spec,
|
||||||
'unit' => (string) ($snapshot->unit ?? $product->unit),
|
'unit' => (string) ($snapshot->unit ?? $product->unit),
|
||||||
'cost_price' => (float) ($snapshot->cost_price ?? $product->cost_price),
|
'cost_price' => (float) ($snapshot->cost_price ?? $product->cost_price),
|
||||||
@@ -172,7 +173,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
|
|||||||
|
|
||||||
// 列头
|
// 列头
|
||||||
$rows[] = array_merge(
|
$rows[] = array_merge(
|
||||||
['序号', '分类', '品名', '供应商', '包规', '单位', '成本', '参考零售价', '数量', '实际称重', '金额'],
|
['序号', '分类', '品名', '供应商', '市场', '包规', '单位', '成本', '参考零售价', '数量', '实际称重', '金额'],
|
||||||
array_values($this->storeNames),
|
array_values($this->storeNames),
|
||||||
);
|
);
|
||||||
$this->specialRows[++$rowIndex] = 'header';
|
$this->specialRows[++$rowIndex] = 'header';
|
||||||
@@ -189,6 +190,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
|
|||||||
$item['category'],
|
$item['category'],
|
||||||
$item['product_name'],
|
$item['product_name'],
|
||||||
$item['supplier'],
|
$item['supplier'],
|
||||||
|
$item['market'],
|
||||||
$item['product_spec'],
|
$item['product_spec'],
|
||||||
$item['unit'],
|
$item['unit'],
|
||||||
$item['cost_price'],
|
$item['cost_price'],
|
||||||
@@ -208,7 +210,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
|
|||||||
|
|
||||||
// 合计行(行合计 + 门店列合计)
|
// 合计行(行合计 + 门店列合计)
|
||||||
$rows[] = array_merge(
|
$rows[] = array_merge(
|
||||||
['', '', '合计', '', '', '', '', '', (float) $totalQuantity, (float) $totalWeight, (float) $totalAmount],
|
['', '', '合计', '', '', '', '', '', '', (float) $totalQuantity, (float) $totalWeight, (float) $totalAmount],
|
||||||
array_values($storeTotals),
|
array_values($storeTotals),
|
||||||
);
|
);
|
||||||
$this->specialRows[++$rowIndex] = 'summary';
|
$this->specialRows[++$rowIndex] = 'summary';
|
||||||
@@ -225,7 +227,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
|
|||||||
$this->collection();
|
$this->collection();
|
||||||
|
|
||||||
$sheet->freezePane('A' . ($this->headerRow + 1));
|
$sheet->freezePane('A' . ($this->headerRow + 1));
|
||||||
$widths = [6, 10, 20, 12, 12, 8, 10, 12, 10, 12, 12];
|
$widths = [6, 10, 20, 12, 12, 12, 8, 10, 12, 10, 12, 12];
|
||||||
foreach ($widths as $index => $width) {
|
foreach ($widths as $index => $width) {
|
||||||
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
||||||
}
|
}
|
||||||
@@ -233,7 +235,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
|
|||||||
// 门店列整列(列头 → 合计行)填充突出颜色
|
// 门店列整列(列头 → 合计行)填充突出颜色
|
||||||
$storeCount = count($this->storeNames);
|
$storeCount = count($this->storeNames);
|
||||||
for ($i = 0; $i < $storeCount; $i++) {
|
for ($i = 0; $i < $storeCount; $i++) {
|
||||||
$column = Coordinate::stringFromColumnIndex(12 + $i);
|
$column = Coordinate::stringFromColumnIndex(13 + $i);
|
||||||
$sheet->getColumnDimension($column)->setWidth(12);
|
$sheet->getColumnDimension($column)->setWidth(12);
|
||||||
$sheet->getStyle($column . $this->headerRow . ':' . $column . $this->lastRow)
|
$sheet->getStyle($column . $this->headerRow . ':' . $column . $this->lastRow)
|
||||||
->getFill()
|
->getFill()
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
|
|||||||
$rowIndex++;
|
$rowIndex++;
|
||||||
|
|
||||||
// 列头
|
// 列头
|
||||||
$rows[] = ['序号', '品名', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额'];
|
$rows[] = ['序号', '品名', '市场', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额'];
|
||||||
$this->specialRows[++$rowIndex] = 'header';
|
$this->specialRows[++$rowIndex] = 'header';
|
||||||
$this->headerRow = $rowIndex;
|
$this->headerRow = $rowIndex;
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
|
|||||||
$rows[] = [
|
$rows[] = [
|
||||||
$sort + 1,
|
$sort + 1,
|
||||||
$item['product_name'],
|
$item['product_name'],
|
||||||
|
$item['market'],
|
||||||
$item['product_spec'],
|
$item['product_spec'],
|
||||||
$item['unit'],
|
$item['unit'],
|
||||||
(float) $item['price'],
|
(float) $item['price'],
|
||||||
@@ -81,7 +82,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 合计行
|
// 合计行
|
||||||
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
|
$rows[] = ['', '合计', '', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
|
||||||
$this->specialRows[++$rowIndex] = 'summary';
|
$this->specialRows[++$rowIndex] = 'summary';
|
||||||
|
|
||||||
return $this->rows = collect($rows);
|
return $this->rows = collect($rows);
|
||||||
@@ -100,7 +101,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
|
|||||||
$this->collection();
|
$this->collection();
|
||||||
|
|
||||||
$sheet->freezePane('A' . ($this->headerRow + 1));
|
$sheet->freezePane('A' . ($this->headerRow + 1));
|
||||||
$widths = [6, 24, 14, 8, 10, 10, 12, 12];
|
$widths = [6, 24, 12, 14, 8, 10, 10, 12, 12];
|
||||||
foreach ($widths as $index => $width) {
|
foreach ($widths as $index => $width) {
|
||||||
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($index + 1))
|
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($index + 1))
|
||||||
->setWidth($width);
|
->setWidth($width);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
|
|||||||
$rowIndex++;
|
$rowIndex++;
|
||||||
|
|
||||||
// 列头
|
// 列头
|
||||||
$rows[] = ['序号', '品名', '包规', '单位', '成本价', '数量', '重量(斤)', '金额'];
|
$rows[] = ['序号', '品名', '市场', '包规', '单位', '成本价', '数量', '重量(斤)', '金额'];
|
||||||
$this->specialRows[++$rowIndex] = 'header';
|
$this->specialRows[++$rowIndex] = 'header';
|
||||||
$this->headerRow = $rowIndex;
|
$this->headerRow = $rowIndex;
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
|
|||||||
$rows[] = [
|
$rows[] = [
|
||||||
$sort + 1,
|
$sort + 1,
|
||||||
$item['product_name'],
|
$item['product_name'],
|
||||||
|
$item['market'],
|
||||||
$item['product_spec'],
|
$item['product_spec'],
|
||||||
$item['unit'],
|
$item['unit'],
|
||||||
(float) $item['cost_price'],
|
(float) $item['cost_price'],
|
||||||
@@ -82,7 +83,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 合计行
|
// 合计行
|
||||||
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
|
$rows[] = ['', '合计', '', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
|
||||||
$this->specialRows[++$rowIndex] = 'summary';
|
$this->specialRows[++$rowIndex] = 'summary';
|
||||||
|
|
||||||
return $this->rows = collect($rows);
|
return $this->rows = collect($rows);
|
||||||
@@ -101,7 +102,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
|
|||||||
$this->collection();
|
$this->collection();
|
||||||
|
|
||||||
$sheet->freezePane('A' . ($this->headerRow + 1));
|
$sheet->freezePane('A' . ($this->headerRow + 1));
|
||||||
$widths = [6, 24, 14, 8, 10, 10, 12, 12];
|
$widths = [6, 24, 12, 14, 8, 10, 10, 12, 12];
|
||||||
foreach ($widths as $index => $width) {
|
foreach ($widths as $index => $width) {
|
||||||
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class ProductController extends BaseController
|
|||||||
'name' => 'like',
|
'name' => 'like',
|
||||||
'category_id' => '=',
|
'category_id' => '=',
|
||||||
'supplier_id' => '=',
|
'supplier_id' => '=',
|
||||||
|
'market' => 'like',
|
||||||
'status' => '=',
|
'status' => '=',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class PurchaseOrderController extends BaseController
|
|||||||
'unit' => $first->unit, // 单位
|
'unit' => $first->unit, // 单位
|
||||||
'supplier_id' => (int) $first->supplier_id, // 供应商id
|
'supplier_id' => (int) $first->supplier_id, // 供应商id
|
||||||
'supplier' => $supplier, // 供应商
|
'supplier' => $supplier, // 供应商
|
||||||
|
'market' => (string) ($product?->market ?? ''), // 市场(取商品档案)
|
||||||
'cost_price' => $first->cost_price, // 成本价
|
'cost_price' => $first->cost_price, // 成本价
|
||||||
'quantity' => $quantity,
|
'quantity' => $quantity,
|
||||||
'weight' => (float) $weight,
|
'weight' => (float) $weight,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ProductFormRequest extends BaseFormRequest
|
|||||||
return [
|
return [
|
||||||
'category_id' => ['required','integer', new Exists(ProductCategoryModel::class,'id')],
|
'category_id' => ['required','integer', new Exists(ProductCategoryModel::class,'id')],
|
||||||
'supplier_id' => ['nullable','integer', 'exclude_if:supplier_id,0', new Exists(SupplierModel::class,'id')],
|
'supplier_id' => ['nullable','integer', 'exclude_if:supplier_id,0', new Exists(SupplierModel::class,'id')],
|
||||||
|
'market' => 'nullable|string|max:50',
|
||||||
'name' => 'required|string|max:100',
|
'name' => 'required|string|max:100',
|
||||||
'spec' => 'nullable|string|max:100',
|
'spec' => 'nullable|string|max:100',
|
||||||
'unit' => 'nullable|string|max:20',
|
'unit' => 'nullable|string|max:20',
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ class ProductImport implements ToCollection
|
|||||||
*/
|
*/
|
||||||
private function parseRow(array $cells, Collection $categories, array $leafIds): array
|
private function parseRow(array $cells, Collection $categories, array $leafIds): array
|
||||||
{
|
{
|
||||||
[$name, $categoryName, $supplierName, $spec, $unit, $costPrice, $sort, $stock, $shelfLife, $statusText, $remark] =
|
[$name, $categoryName, $supplierName, $market, $spec, $unit, $costPrice, $sort, $stock, $shelfLife, $statusText, $remark] =
|
||||||
array_pad(array_slice($cells, 0, 11), 11, null);
|
array_pad(array_slice($cells, 0, 12), 12, null);
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
@@ -120,6 +120,11 @@ class ProductImport implements ToCollection
|
|||||||
$errors[] = '供应商名称最长 100 个字符';
|
$errors[] = '供应商名称最长 100 个字符';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$market = (string) $market;
|
||||||
|
if (mb_strlen($market) > 50) {
|
||||||
|
$errors[] = '市场最长 50 个字符';
|
||||||
|
}
|
||||||
|
|
||||||
$spec = (string) $spec;
|
$spec = (string) $spec;
|
||||||
if (mb_strlen($spec) > 100) {
|
if (mb_strlen($spec) > 100) {
|
||||||
$errors[] = '规格/包规最长 100 个字符';
|
$errors[] = '规格/包规最长 100 个字符';
|
||||||
@@ -169,6 +174,7 @@ class ProductImport implements ToCollection
|
|||||||
'category_id' => $categoryId,
|
'category_id' => $categoryId,
|
||||||
'supplier_id' => 0, // 写入阶段按 supplier_name 解析(匹配或自动创建)
|
'supplier_id' => 0, // 写入阶段按 supplier_name 解析(匹配或自动创建)
|
||||||
'supplier_name' => $supplierName, // 写入阶段解析,不入 product 表
|
'supplier_name' => $supplierName, // 写入阶段解析,不入 product 表
|
||||||
|
'market' => $market,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'spec' => $spec,
|
'spec' => $spec,
|
||||||
'unit' => $unit,
|
'unit' => $unit,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class ProductModel extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'category_id',
|
'category_id',
|
||||||
'supplier_id',
|
'supplier_id',
|
||||||
|
'market',
|
||||||
'name',
|
'name',
|
||||||
'spec',
|
'spec',
|
||||||
'unit',
|
'unit',
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class PurchaseItemService
|
|||||||
'product_name' => $first->product_name,
|
'product_name' => $first->product_name,
|
||||||
'product_spec' => $first->product_spec,
|
'product_spec' => $first->product_spec,
|
||||||
'unit' => $first->unit,
|
'unit' => $first->unit,
|
||||||
|
'market' => (string) data_get($first, 'market', ''),
|
||||||
// 加权平均单价(保证 单价×数量=预计金额)
|
// 加权平均单价(保证 单价×数量=预计金额)
|
||||||
'price' => $quantity > 0
|
'price' => $quantity > 0
|
||||||
? bcdiv($amount, (string) $quantity, 2)
|
? bcdiv($amount, (string) $quantity, 2)
|
||||||
@@ -124,6 +125,7 @@ class PurchaseItemService
|
|||||||
'product_name' => $first->product_name,
|
'product_name' => $first->product_name,
|
||||||
'product_spec' => $first->product_spec,
|
'product_spec' => $first->product_spec,
|
||||||
'unit' => $first->unit,
|
'unit' => $first->unit,
|
||||||
|
'market' => (string) data_get($first, 'market', ''),
|
||||||
'cost_price' => (string) $first->cost_price,
|
'cost_price' => (string) $first->cost_price,
|
||||||
'quantity' => $quantity,
|
'quantity' => $quantity,
|
||||||
'weight' => $weight,
|
'weight' => $weight,
|
||||||
@@ -151,17 +153,18 @@ class PurchaseItemService
|
|||||||
->get()
|
->get()
|
||||||
->makeVisible('cost_price');
|
->makeVisible('cost_price');
|
||||||
|
|
||||||
// 排序键:商品分类 sort → 商品 sort(商品含软删除,保证历史单据可导出)
|
// 排序键:商品分类 sort → 商品 sort;市场取商品档案(商品含软删除,保证历史单据可导出)
|
||||||
$products = ProductModel::withTrashed()
|
$products = ProductModel::withTrashed()
|
||||||
->with('category:id,sort')
|
->with('category:id,sort')
|
||||||
->whereIn('id', $items->pluck('product_id')->unique())
|
->whereIn('id', $items->pluck('product_id')->unique())
|
||||||
->get(['id', 'category_id', 'sort'])
|
->get(['id', 'category_id', 'sort', 'market'])
|
||||||
->keyBy('id');
|
->keyBy('id');
|
||||||
|
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$product = $products->get((int) $item->product_id);
|
$product = $products->get((int) $item->product_id);
|
||||||
$item->setAttribute('category_sort', (int) ($product?->category?->sort ?? 9999));
|
$item->setAttribute('category_sort', (int) ($product?->category?->sort ?? 9999));
|
||||||
$item->setAttribute('product_sort', (int) ($product?->sort ?? 9999));
|
$item->setAttribute('product_sort', (int) ($product?->sort ?? 9999));
|
||||||
|
$item->setAttribute('market', (string) ($product?->market ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $items;
|
return $items;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class ProductModelFactory extends Factory
|
|||||||
return [
|
return [
|
||||||
'category_id' => 0,
|
'category_id' => 0,
|
||||||
'supplier_id' => 0,
|
'supplier_id' => 0,
|
||||||
|
'market' => '',
|
||||||
'name' => self::NAMES[$seq % count(self::NAMES)] . $seq,
|
'name' => self::NAMES[$seq % count(self::NAMES)] . $seq,
|
||||||
'spec' => self::SPECS[$seq % count(self::SPECS)],
|
'spec' => self::SPECS[$seq % count(self::SPECS)],
|
||||||
'unit' => self::UNITS[$seq % count(self::UNITS)],
|
'unit' => self::UNITS[$seq % count(self::UNITS)],
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ return new class extends Migration
|
|||||||
$table->increments('id')->comment('商品ID');
|
$table->increments('id')->comment('商品ID');
|
||||||
$table->integer('category_id')->default(0)->comment('商品分类ID');
|
$table->integer('category_id')->default(0)->comment('商品分类ID');
|
||||||
$table->integer('supplier_id')->default(0)->comment('默认供应商ID');
|
$table->integer('supplier_id')->default(0)->comment('默认供应商ID');
|
||||||
|
$table->string('market', 50)->default('')->comment('市场(如:新发地、岳各庄)');
|
||||||
$table->string('name', 100)->comment('品名');
|
$table->string('name', 100)->comment('品名');
|
||||||
$table->string('spec', 100)->default('')->comment('规格/包规');
|
$table->string('spec', 100)->default('')->comment('规格/包规');
|
||||||
$table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)');
|
$table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)');
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
# 小程序端修改文档(门店端)
|
|
||||||
|
|
||||||
> 版本:2026-08-17
|
|
||||||
> 后端变更:用户表与门店表合并(一个用户就是一个门店),门店登录由「微信静默登录」改为「账号 + 密码登录」。
|
|
||||||
> 本文档面向小程序(门店端)开发者,说明需要配合修改的内容。**未列出的接口均无变化**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、变更总览
|
|
||||||
|
|
||||||
| 事项 | 旧 | 新 |
|
|
||||||
|------|-----|-----|
|
|
||||||
| 登录方式 | `wx.login` 拿 code,调 `/mini/auth/login` 静默登录 | 账号 + 密码登录(账号由商家后台分配) |
|
|
||||||
| 注册 | `POST /mini/auth/register`(微信 code + 手机号 + 门店编码) | **接口下线**,门店账号由商家在 PC 后台创建 |
|
|
||||||
| 登录主体 | 微信用户(user),再绑定门店(store) | **门店即用户**:登录成功返回的就是门店本身 |
|
|
||||||
| 修改密码 | 无 | 新增 `PUT /mini/auth/password` |
|
|
||||||
| Token 鉴权 | Bearer Token | **不变** |
|
|
||||||
| 其他业务接口 | 购物车 / 订单 / 账单 / 支付 / 通知 / 商品 / 首页 | **全部不变**(路径、入参、出参) |
|
|
||||||
|
|
||||||
### 需要小程序端配合的改动
|
|
||||||
|
|
||||||
1. **重做登录页**:去掉 `wx.login` 流程,改为账号 + 密码表单(账号密码由商家线下告知门店)。
|
|
||||||
2. **删除注册页/绑定门店页**:注册接口已下线,「尚未绑定门店」的场景已不存在。
|
|
||||||
3. **用户信息结构调整**:登录与 `auth/info` 返回的对象字段变化(见下文)。
|
|
||||||
4. **新增「修改密码」入口**(建议放在「我的」页面)。
|
|
||||||
5. **重新登录**:旧 token 全部失效,需引导用户用账号密码重新登录一次。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、登录接口(变更)
|
|
||||||
|
|
||||||
### `POST /mini/auth/login`
|
|
||||||
|
|
||||||
**请求参数(变更):**
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| username | string | 是 | 登录账号(商家后台分配,4~20 位) |
|
|
||||||
| password | string | 是 | 登录密码 |
|
|
||||||
|
|
||||||
> 旧参数 `code`(wx.login 凭证)已废弃,无需再传,也无需调用 `wx.login`。
|
|
||||||
|
|
||||||
**成功响应:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"msg": "登录成功",
|
|
||||||
"data": {
|
|
||||||
"token": "1|xxxxxxxxxxxxxxxx",
|
|
||||||
"user": {
|
|
||||||
"id": 3,
|
|
||||||
"name": "好又多超市",
|
|
||||||
"code": "S000003",
|
|
||||||
"username": "hyd001",
|
|
||||||
"avatar": "",
|
|
||||||
"level_id": 1,
|
|
||||||
"level": { "id": 1, "name": "一级客户" },
|
|
||||||
"contact": "张三",
|
|
||||||
"phone": "13800000000",
|
|
||||||
"address": "xx路 1 号",
|
|
||||||
"payment_cycle_days": 2,
|
|
||||||
"status": 1,
|
|
||||||
"last_login_at": "2026-08-17 10:00:00",
|
|
||||||
"created_at": "2026-08-01 09:00:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**user 字段变化要点:**
|
|
||||||
|
|
||||||
- `user` 现在**就是门店对象**(扁平结构),不再有 `user.store` 嵌套,也不再有 `openid`、`unionid`、`nickname`、`email`、`store_id` 字段。
|
|
||||||
- 门店名称取 `user.name`(原 `user.store.name`);客户等级取 `user.level`。
|
|
||||||
- `password` 字段永不返回。
|
|
||||||
|
|
||||||
**失败响应(`success: false`,`msg` 提示):**
|
|
||||||
|
|
||||||
| 场景 | msg |
|
|
||||||
|------|-----|
|
|
||||||
| 账号不存在或密码错误 | 账号或密码错误 |
|
|
||||||
| 门店已停用 | 账号已被停用,请联系客服处理 |
|
|
||||||
| 参数缺失 | 请输入登录账号 / 请输入登录密码 |
|
|
||||||
|
|
||||||
**Token 使用(不变):** 后续所有请求携带请求头 `Authorization: Bearer {token}`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、注册接口(下线)
|
|
||||||
|
|
||||||
### ~~`POST /mini/auth/register`~~ —— 已删除
|
|
||||||
|
|
||||||
- 门店账号全部由商家在 PC 后台「客户管理 → 门店管理」创建并分配(含登录账号、初始密码)。
|
|
||||||
- 小程序端请移除注册页、「输入门店编码绑定」页及相关逻辑。
|
|
||||||
- 同步移除 `wx.getPhoneNumber` 手机号授权流程(后端已不再使用)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、当前用户信息(结构变化)
|
|
||||||
|
|
||||||
### `GET /mini/auth/info`(需登录)
|
|
||||||
|
|
||||||
返回当前登录门店完整信息(含 `level` 等级嵌套),字段与登录接口的 `user` 一致(见上文示例)。原来读取 `info.store.xxx` 的地方改为直接读 `info.xxx`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、修改密码(新增)
|
|
||||||
|
|
||||||
### `PUT /mini/auth/password`(需登录)
|
|
||||||
|
|
||||||
**请求参数:**
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| oldPassword | string | 是 | 原密码 |
|
|
||||||
| newPassword | string | 是 | 新密码(6~20 位) |
|
|
||||||
| rePassword | string | 是 | 确认新密码(须与 newPassword 一致) |
|
|
||||||
|
|
||||||
**失败场景:** 原密码不正确 / 两次输入的密码不一致 / 新密码至少 6 位。
|
|
||||||
|
|
||||||
> 修改密码成功后现有 token 仍然有效,无需强制重新登录。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、门店信息(不变)
|
|
||||||
|
|
||||||
### `GET /mini/store/info`(需登录)
|
|
||||||
|
|
||||||
返回:`{ id, name, code, contact, phone, address, payment_cycle_days }`(门店名称/编码/回款周期只读,由后台维护)。
|
|
||||||
|
|
||||||
### `PUT /mini/store/info`(需登录)
|
|
||||||
|
|
||||||
可改字段不变:`contact`(联系人)、`phone`(联系电话)、`address`(地址)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、以下接口完全不变
|
|
||||||
|
|
||||||
路径、入参、出参均无变化,仅内部归属从「用户」变为「门店」(对小程序透明):
|
|
||||||
|
|
||||||
| 模块 | 接口 |
|
|
||||||
|------|------|
|
|
||||||
| 首页 | `GET /mini/home/*`(轮播/导航/促销) |
|
|
||||||
| 商品 | `GET /mini/product/categories`、`GET /mini/product/list`、`GET /mini/product/{id}`(登录后按本店等级显示价格) |
|
|
||||||
| 购物车 | `POST /mini/cart`、`GET /mini/cart`、`PUT /mini/cart/{id}`、`DELETE /mini/cart/{id}`、`DELETE /mini/cart` |
|
|
||||||
| 订单 | `POST /mini/order`、`GET /mini/order`、`GET /mini/order/summary`、`GET /mini/order/{id}`、`PUT /mini/order/{id}/cancel` |
|
|
||||||
| 账单 | `GET /mini/bill`、`GET /mini/bill/{id}`、`GET /mini/bill/export` |
|
|
||||||
| 支付 | `GET /mini/payment/config`、`GET /mini/payment`、`POST /mini/payment`、`GET /mini/payment/{id}` |
|
|
||||||
| 通知 | `GET /mini/notice`、`PUT /mini/notice/{id}/read` |
|
|
||||||
| 上传 | `POST /mini/upload` |
|
|
||||||
|
|
||||||
> 唯一语义差异:数据隔离现在天然按门店划分(一个门店一个账号),原「同一门店多个微信账号各自购物车」的合并场景不再存在。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、错误语义(不变)
|
|
||||||
|
|
||||||
- 未携带/无效 token:HTTP `401`。
|
|
||||||
- 业务错误:HTTP `200` + `{ success: false, msg: "..." }`,直接 toast `msg` 即可。
|
|
||||||
- 「门店未设置客户等级,无法加购/下单,请联系客服」等提示文案不变。
|
|
||||||
- 「尚未绑定门店」提示已移除(该场景不存在)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、上线 Checklist(小程序端)
|
|
||||||
|
|
||||||
- [ ] 登录页改为账号 + 密码表单,移除 `wx.login` / `wx.getPhoneNumber` 调用
|
|
||||||
- [ ] 移除注册页、门店绑定页及路由
|
|
||||||
- [ ] 全局用户信息读取点从 `user.store.*` 调整为 `user.*`(门店名、等级、回款周期等)
|
|
||||||
- [ ] 「我的」页面新增修改密码入口
|
|
||||||
- [ ] 旧版本缓存的 token 失效处理:401 时引导重新登录
|
|
||||||
- [ ] 等级价格展示逻辑不变(接口返回的 `price` 字段含义不变)
|
|
||||||
@@ -108,12 +108,14 @@ class ExportTest extends ProcurementTestCase
|
|||||||
'cost_price' => '5.00',
|
'cost_price' => '5.00',
|
||||||
'spec' => '10斤/箱',
|
'spec' => '10斤/箱',
|
||||||
'supplier_id' => $supplierA->id,
|
'supplier_id' => $supplierA->id,
|
||||||
|
'market' => '新发地',
|
||||||
]);
|
]);
|
||||||
$meat = ProductModel::factory()->create([
|
$meat = ProductModel::factory()->create([
|
||||||
'status' => ProductModel::STATUS_ON,
|
'status' => ProductModel::STATUS_ON,
|
||||||
'cost_price' => '20.00',
|
'cost_price' => '20.00',
|
||||||
'spec' => '20斤/箱',
|
'spec' => '20斤/箱',
|
||||||
'supplier_id' => $supplierB->id,
|
'supplier_id' => $supplierB->id,
|
||||||
|
'market' => '岳各庄',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAsMiniStore($storeA);
|
$this->actingAsMiniStore($storeA);
|
||||||
@@ -154,24 +156,25 @@ class ExportTest extends ProcurementTestCase
|
|||||||
if ($rows->count() !== 8) {
|
if ($rows->count() !== 8) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 列头含参考零售价与门店列
|
// 列头含市场/参考零售价与门店列(市场列在供应商后,门店列从索引 12 起)
|
||||||
$header = $rows[3];
|
$header = $rows[3];
|
||||||
if ($header[7] !== '参考零售价' || $header[11] !== $storeA->name || $header[12] !== $storeB->name) {
|
if ($header[4] !== '市场' || $header[8] !== '参考零售价' || $header[12] !== $storeA->name || $header[13] !== $storeB->name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 蔬菜行:数量 2+3=5、金额 25、参考零售价 5÷10=0.50、门店列 2/3
|
// 蔬菜行:市场 新发地、数量 2+3=5、金额 25、参考零售价 5÷10=0.50、门店列 2/3
|
||||||
$vegRow = $rows->firstWhere(2, $veg->name);
|
$vegRow = $rows->firstWhere(2, $veg->name);
|
||||||
if ($vegRow === null
|
if ($vegRow === null
|
||||||
|| (float) $vegRow[8] !== 5.0 || (float) $vegRow[10] !== 25.0
|
|| $vegRow[4] !== '新发地'
|
||||||
|| (float) $vegRow[7] !== 0.5
|
|| (float) $vegRow[9] !== 5.0 || (float) $vegRow[11] !== 25.0
|
||||||
|| (float) $vegRow[11] !== 2.0 || (float) $vegRow[12] !== 3.0) {
|
|| (float) $vegRow[8] !== 0.5
|
||||||
|
|| (float) $vegRow[12] !== 2.0 || (float) $vegRow[13] !== 3.0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 无订货商品行:数量 0、参考零售价留空、门店列 0
|
// 无订货商品行:数量 0、参考零售价留空、门店列 0
|
||||||
$extraRow = $rows->firstWhere(2, $extra->name);
|
$extraRow = $rows->firstWhere(2, $extra->name);
|
||||||
if ($extraRow === null
|
if ($extraRow === null
|
||||||
|| (float) $extraRow[8] !== 0.0 || $extraRow[7] !== ''
|
|| (float) $extraRow[9] !== 0.0 || $extraRow[8] !== ''
|
||||||
|| (float) $extraRow[11] !== 0.0) {
|
|| (float) $extraRow[12] !== 0.0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 合计行:总数量 6、总金额 45、门店列合计 3/3
|
// 合计行:总数量 6、总金额 45、门店列合计 3/3
|
||||||
@@ -179,8 +182,8 @@ class ExportTest extends ProcurementTestCase
|
|||||||
if ($total[2] !== '合计') {
|
if ($total[2] !== '合计') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (float) $total[8] === 6.0 && (float) $total[10] === 45.0
|
return (float) $total[9] === 6.0 && (float) $total[11] === 45.0
|
||||||
&& (float) $total[11] === 3.0 && (float) $total[12] === 3.0;
|
&& (float) $total[12] === 3.0 && (float) $total[13] === 3.0;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -234,12 +237,12 @@ class ExportTest extends ProcurementTestCase
|
|||||||
if ($rows->count() !== 6) {
|
if ($rows->count() !== 6) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ($rows[2] !== ['序号', '品名', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额']) {
|
if ($rows[2] !== ['序号', '品名', '市场', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 合计:数量 2+1=3,金额 10+20=30
|
// 合计:数量 2+1=3,金额 10+20=30
|
||||||
$total = $rows->last();
|
$total = $rows->last();
|
||||||
return $total[1] === '合计' && (int) $total[5] === 3 && (float) $total[7] === 30.0;
|
return $total[1] === '合计' && (int) $total[6] === 3 && (float) $total[8] === 30.0;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -294,23 +297,23 @@ class ExportTest extends ProcurementTestCase
|
|||||||
if ($titles !== [$supplierA->name, $supplierB->name]) {
|
if ($titles !== [$supplierA->name, $supplierB->name]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 供应商甲:蔬菜 2+3=5 件、金额 5×5=25
|
// 供应商甲:蔬菜 2+3=5 件、金额 5×5=25(市场列在品名后)
|
||||||
$rowsA = $sheets[0]->collection()->values();
|
$rowsA = $sheets[0]->collection()->values();
|
||||||
if ($rowsA[2] !== ['序号', '品名', '包规', '单位', '成本价', '数量', '重量(斤)', '金额']) {
|
if ($rowsA[2] !== ['序号', '品名', '市场', '包规', '单位', '成本价', '数量', '重量(斤)', '金额']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$vegRow = $rowsA->firstWhere(1, $veg->name);
|
$vegRow = $rowsA->firstWhere(1, $veg->name);
|
||||||
if ($vegRow === null || (int) $vegRow[5] !== 5 || (float) $vegRow[7] !== 25.0) {
|
if ($vegRow === null || $vegRow[2] !== '新发地' || (int) $vegRow[6] !== 5 || (float) $vegRow[8] !== 25.0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$totalA = $rowsA->last();
|
$totalA = $rowsA->last();
|
||||||
if ($totalA[1] !== '合计' || (int) $totalA[5] !== 5 || (float) $totalA[7] !== 25.0) {
|
if ($totalA[1] !== '合计' || (int) $totalA[6] !== 5 || (float) $totalA[8] !== 25.0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 供应商乙:肉 1 件、金额 20
|
// 供应商乙:肉 1 件、金额 20
|
||||||
$rowsB = $sheets[1]->collection()->values();
|
$rowsB = $sheets[1]->collection()->values();
|
||||||
$meatRow = $rowsB->firstWhere(1, $meat->name);
|
$meatRow = $rowsB->firstWhere(1, $meat->name);
|
||||||
return $meatRow !== null && (int) $meatRow[5] === 1 && (float) $meatRow[7] === 20.0;
|
return $meatRow !== null && $meatRow[2] === '岳各庄' && (int) $meatRow[6] === 1 && (float) $meatRow[8] === 20.0;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
ProductModel::factory()->create([
|
ProductModel::factory()->create([
|
||||||
'category_id' => $leaf->id,
|
'category_id' => $leaf->id,
|
||||||
'supplier_id' => $supplier->id,
|
'supplier_id' => $supplier->id,
|
||||||
|
'market' => '新发地',
|
||||||
'name' => '上海青',
|
'name' => '上海青',
|
||||||
'spec' => '10斤/箱',
|
'spec' => '10斤/箱',
|
||||||
'unit' => '斤',
|
'unit' => '斤',
|
||||||
@@ -93,19 +94,19 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
if ($row === null) {
|
if ($row === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// [品名, 分类路径, 供应商, 规格, 单位, 成本价, 排序, 库存, 保质期, 状态, 备注]
|
// [品名, 分类路径, 供应商, 市场, 规格, 单位, 成本价, 排序, 库存, 保质期, 状态, 备注]
|
||||||
if ($row[1] !== '蔬菜/叶菜类' || $row[2] !== '张三蔬菜批发' || $row[9] !== '上架') {
|
if ($row[1] !== '蔬菜/叶菜类' || $row[2] !== '张三蔬菜批发' || $row[3] !== '新发地' || $row[10] !== '上架') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((float) $row[5] !== 2.5 || (int) $row[6] !== 3 || (int) $row[7] !== 100 || (int) $row[8] !== 2) {
|
if ((float) $row[6] !== 2.5 || (int) $row[7] !== 3 || (int) $row[8] !== 100 || (int) $row[9] !== 2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ($row[10] !== '新鲜直达') {
|
if ($row[11] !== '新鲜直达') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$offRow = $rows->firstWhere(0, '下架商品');
|
$offRow = $rows->firstWhere(0, '下架商品');
|
||||||
return $offRow !== null && $offRow[9] === '下架' && $offRow[2] === '';
|
return $offRow !== null && $offRow[10] === '下架' && $offRow[2] === '' && $offRow[3] === '';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -164,6 +165,22 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
$this->assertFalse($response->json('success'), '缺少导出权限点应被拦截');
|
$this->assertFalse($response->json('success'), '缺少导出权限点应被拦截');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 列表按市场模糊筛选 */
|
||||||
|
public function test_list_filters_by_market(): void
|
||||||
|
{
|
||||||
|
ProductModel::factory()->create(['name' => '大白菜X', 'market' => '新发地']);
|
||||||
|
ProductModel::factory()->create(['name' => '土豆X', 'market' => '岳各庄']);
|
||||||
|
ProductModel::factory()->create(['name' => '无市场商品X', 'market' => '']);
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
$response = $this->getJson('/product/goods?market=新发');
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$names = collect($response->json('data.data'))->pluck('name');
|
||||||
|
$this->assertContains('大白菜X', $names);
|
||||||
|
$this->assertNotContains('土豆X', $names);
|
||||||
|
$this->assertNotContains('无市场商品X', $names);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 导入 ====================
|
// ==================== 导入 ====================
|
||||||
|
|
||||||
/** 正常导入:新增商品 + 供应商自动创建 + 默认值(单位斤/状态上架) */
|
/** 正常导入:新增商品 + 供应商自动创建 + 默认值(单位斤/状态上架) */
|
||||||
@@ -174,8 +191,8 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$file = $this->makeUploadFile([
|
$file = $this->makeUploadFile([
|
||||||
ProductExport::HEADERS,
|
ProductExport::HEADERS,
|
||||||
['上海青', '叶菜类', '新供应商A', '10斤/箱', '', 2.5, 3, 100, 2, '下架', '备注信息'],
|
['上海青', '叶菜类', '新供应商A', '新发地', '10斤/箱', '', 2.5, 3, 100, 2, '下架', '备注信息'],
|
||||||
['西红柿', '蔬菜/茄果类', '', '散装', '箱', 1.8, '', '', '', '', ''],
|
['西红柿', '蔬菜/茄果类', '', '', '散装', '箱', 1.8, '', '', '', '', ''],
|
||||||
]);
|
]);
|
||||||
// 第二行用到了不存在的「茄果类」路径 → 先补建该分类
|
// 第二行用到了不存在的「茄果类」路径 → 先补建该分类
|
||||||
ProductCategoryModel::create(['name' => '茄果类', 'parent_id' => $leaf->parent_id, 'sort' => 1, 'status' => 1]);
|
ProductCategoryModel::create(['name' => '茄果类', 'parent_id' => $leaf->parent_id, 'sort' => 1, 'status' => 1]);
|
||||||
@@ -187,6 +204,7 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
$productA = ProductModel::where('name', '上海青')->first();
|
$productA = ProductModel::where('name', '上海青')->first();
|
||||||
$this->assertNotNull($productA);
|
$this->assertNotNull($productA);
|
||||||
$this->assertSame($leaf->id, $productA->category_id);
|
$this->assertSame($leaf->id, $productA->category_id);
|
||||||
|
$this->assertSame('新发地', $productA->market);
|
||||||
$this->assertSame('斤', $productA->unit, '单位留空应默认斤');
|
$this->assertSame('斤', $productA->unit, '单位留空应默认斤');
|
||||||
$this->assertSame(ProductModel::STATUS_OFF, $productA->status);
|
$this->assertSame(ProductModel::STATUS_OFF, $productA->status);
|
||||||
$this->assertSame('2.50', (string) $productA->cost_price);
|
$this->assertSame('2.50', (string) $productA->cost_price);
|
||||||
@@ -197,6 +215,7 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
|
|
||||||
$productB = ProductModel::where('name', '西红柿')->first();
|
$productB = ProductModel::where('name', '西红柿')->first();
|
||||||
$this->assertNotNull($productB);
|
$this->assertNotNull($productB);
|
||||||
|
$this->assertSame('', $productB->market, '市场留空应为空字符串');
|
||||||
$this->assertSame(0, $productB->supplier_id, '供应商留空应为 0(不创建)');
|
$this->assertSame(0, $productB->supplier_id, '供应商留空应为 0(不创建)');
|
||||||
$this->assertSame(ProductModel::STATUS_ON, $productB->status, '状态留空应默认上架');
|
$this->assertSame(ProductModel::STATUS_ON, $productB->status, '状态留空应默认上架');
|
||||||
$this->assertSame(0, $productB->sort);
|
$this->assertSame(0, $productB->sort);
|
||||||
@@ -211,7 +230,7 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$file = $this->makeUploadFile([
|
$file = $this->makeUploadFile([
|
||||||
ProductExport::HEADERS,
|
ProductExport::HEADERS,
|
||||||
['黄瓜', '叶菜类', '老供应商', '散装', '斤', 1.2, 0, 0, 0, '上架', ''],
|
['黄瓜', '叶菜类', '老供应商', '', '散装', '斤', 1.2, 0, 0, 0, '上架', ''],
|
||||||
]);
|
]);
|
||||||
$this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true);
|
$this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true);
|
||||||
|
|
||||||
@@ -227,10 +246,10 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$file = $this->makeUploadFile([
|
$file = $this->makeUploadFile([
|
||||||
ProductExport::HEADERS,
|
ProductExport::HEADERS,
|
||||||
['正常商品', '叶菜类', '供应商X', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
['正常商品', '叶菜类', '供应商X', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
||||||
['', '叶菜类', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
['', '叶菜类', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
||||||
['坏商品A', '不存在的分类', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
['坏商品A', '不存在的分类', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
||||||
['坏商品B', '叶菜类', '', '散装', '斤', 'abc', 0, 0, 0, '上架', ''],
|
['坏商品B', '叶菜类', '', '', '散装', '斤', 'abc', 0, 0, 0, '上架', ''],
|
||||||
]);
|
]);
|
||||||
$response = $this->post('/product/goods/import', ['file' => $file]);
|
$response = $this->post('/product/goods/import', ['file' => $file]);
|
||||||
|
|
||||||
@@ -260,16 +279,17 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
// 路径写法命中蔬菜/叶菜
|
// 路径写法命中蔬菜/叶菜
|
||||||
$file = $this->makeUploadFile([
|
$file = $this->makeUploadFile([
|
||||||
ProductExport::HEADERS,
|
ProductExport::HEADERS,
|
||||||
['菠菜', '蔬菜/叶菜', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
['菠菜', '蔬菜/叶菜', '', '岳各庄', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
||||||
]);
|
]);
|
||||||
$this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true);
|
$this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true);
|
||||||
$this->assertSame($vegLeaf->id, ProductModel::where('name', '菠菜')->value('category_id'));
|
$this->assertSame($vegLeaf->id, ProductModel::where('name', '菠菜')->value('category_id'));
|
||||||
|
$this->assertSame('岳各庄', ProductModel::where('name', '菠菜')->value('market'));
|
||||||
|
|
||||||
// 单名称歧义 + 非末级分类 → 报错零写入
|
// 单名称歧义 + 非末级分类 → 报错零写入
|
||||||
$file = $this->makeUploadFile([
|
$file = $this->makeUploadFile([
|
||||||
ProductExport::HEADERS,
|
ProductExport::HEADERS,
|
||||||
['歧义商品', '叶菜', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
['歧义商品', '叶菜', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
||||||
['非末级商品', '蔬菜', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
['非末级商品', '蔬菜', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
||||||
]);
|
]);
|
||||||
$response = $this->post('/product/goods/import', ['file' => $file]);
|
$response = $this->post('/product/goods/import', ['file' => $file]);
|
||||||
$response->assertJsonPath('success', false);
|
$response->assertJsonPath('success', false);
|
||||||
@@ -299,7 +319,7 @@ class ProductImportExportTest extends ProcurementTestCase
|
|||||||
|
|
||||||
$file = $this->makeUploadFile([
|
$file = $this->makeUploadFile([
|
||||||
ProductExport::HEADERS,
|
ProductExport::HEADERS,
|
||||||
['上海青', '叶菜类', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
['上海青', '叶菜类', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
|
||||||
]);
|
]);
|
||||||
$response = $this->post('/product/goods/import', ['file' => $file]);
|
$response = $this->post('/product/goods/import', ['file' => $file]);
|
||||||
$this->assertFalse($response->json('success'), '缺少导入权限点应被拦截');
|
$this->assertFalse($response->json('success'), '缺少导入权限点应被拦截');
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export default interface IProduct {
|
|||||||
category_id?: number;
|
category_id?: number;
|
||||||
/** 供应商ID */
|
/** 供应商ID */
|
||||||
supplier_id?: number;
|
supplier_id?: number;
|
||||||
|
/** 市场(如:新发地、岳各庄) */
|
||||||
|
market?: string;
|
||||||
/** 商品名称 */
|
/** 商品名称 */
|
||||||
name?: string;
|
name?: string;
|
||||||
/** 规格/包规 */
|
/** 规格/包规 */
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface IPurchaseDetailRow {
|
|||||||
unit: string;
|
unit: string;
|
||||||
supplier_id: number;
|
supplier_id: number;
|
||||||
supplier?: { id: number; name: string } | null;
|
supplier?: { id: number; name: string } | null;
|
||||||
|
/** 市场(取商品档案) */
|
||||||
|
market?: string;
|
||||||
/** 成本 */
|
/** 成本 */
|
||||||
cost_price: number;
|
cost_price: number;
|
||||||
/** 合计数量 */
|
/** 合计数量 */
|
||||||
@@ -35,6 +37,8 @@ export interface IPurchaseSupplierItem {
|
|||||||
/** 规格/包规 */
|
/** 规格/包规 */
|
||||||
product_spec: string;
|
product_spec: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
/** 市场(取商品档案) */
|
||||||
|
market?: string;
|
||||||
/** 成本价 */
|
/** 成本价 */
|
||||||
cost_price: string;
|
cost_price: string;
|
||||||
/** 数量(包数) */
|
/** 数量(包数) */
|
||||||
@@ -193,6 +197,8 @@ export interface IPurchaseStoreItem {
|
|||||||
/** 规格/包规 */
|
/** 规格/包规 */
|
||||||
product_spec: string;
|
product_spec: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
/** 市场(取商品档案) */
|
||||||
|
market?: string;
|
||||||
/** 单价(每包;多笔单价时为加权平均,保证 单价×数量=预计金额) */
|
/** 单价(每包;多笔单价时为加权平均,保证 单价×数量=预计金额) */
|
||||||
price: string;
|
price: string;
|
||||||
/** 数量(包数) */
|
/** 数量(包数) */
|
||||||
|
|||||||
@@ -401,6 +401,18 @@ const ProductGoodsPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
render: (_, record) => record.supplier?.name ?? '-',
|
render: (_, record) => record.supplier?.name ?? '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '市场',
|
||||||
|
dataIndex: 'market',
|
||||||
|
valueType: 'text',
|
||||||
|
align: 'center',
|
||||||
|
fieldProps: {
|
||||||
|
placeholder: '市场名称,如:新发地',
|
||||||
|
maxLength: 50,
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
render: (_, record) => record.market || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
@@ -552,7 +564,7 @@ const ProductGoodsPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
列格式:品名*、分类*(末级分类,支持「父分类/子分类」路径)、供应商(不存在自动创建)、
|
列格式:品名*、分类*(末级分类,支持「父分类/子分类」路径)、供应商(不存在自动创建)、市场、
|
||||||
规格/包规、单位(默认斤)、成本价、排序、库存、保质期、状态(上架/下架)、备注。
|
规格/包规、单位(默认斤)、成本价、排序、库存、保质期、状态(上架/下架)、备注。
|
||||||
导入一律新增商品;整表校验,任何一行有错则全部不导入。
|
导入一律新增商品;整表校验,任何一行有错则全部不导入。
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
product_name: row.product_name,
|
product_name: row.product_name,
|
||||||
product_spec: row.product_spec,
|
product_spec: row.product_spec,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
|
market: row.market,
|
||||||
cost_price: String(row.cost_price),
|
cost_price: String(row.cost_price),
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
weight: String(row.weight),
|
weight: String(row.weight),
|
||||||
@@ -474,6 +475,7 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, row) => row.supplier?.name ?? '-',
|
render: (_, row) => row.supplier?.name ?? '-',
|
||||||
},
|
},
|
||||||
|
{ title: '市场', dataIndex: 'market', width: 90, align: 'center', render: (v) => v || '-' },
|
||||||
{
|
{
|
||||||
title: '参考零售价',
|
title: '参考零售价',
|
||||||
key: 'retail_price',
|
key: 'retail_price',
|
||||||
@@ -558,27 +560,28 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Summary.Row>
|
<Table.Summary.Row>
|
||||||
<Table.Summary.Cell index={0} colSpan={6} align="center">
|
<Table.Summary.Cell index={0} colSpan={7} align="center">
|
||||||
<Text strong>成本统计(按门店)</Text>
|
<Text strong>成本统计(按门店)</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
{storeTotals.map((amount, index) => (
|
{storeTotals.map((amount, index) => (
|
||||||
<Table.Summary.Cell key={stores[index].id} index={6 + index} align="center">
|
<Table.Summary.Cell key={stores[index].id} index={7 + index} align="center">
|
||||||
<Text strong>¥{amount.toFixed(2)}</Text>
|
<Text strong>¥{amount.toFixed(2)}</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
))}
|
))}
|
||||||
<Table.Summary.Cell index={6 + stores.length} align="center">
|
<Table.Summary.Cell index={7 + stores.length} align="center">
|
||||||
<Text strong>{totalQuantity}</Text>
|
<Text strong>{totalQuantity}</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={7 + stores.length} align="center">
|
<Table.Summary.Cell index={8 + stores.length} align="center">
|
||||||
<Text strong>¥{totalAmount.toFixed(2)}</Text>
|
<Text strong>¥{totalAmount.toFixed(2)}</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
</Table.Summary.Row>
|
</Table.Summary.Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 门店购买详情列:商品/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 + 操作 */
|
/** 门店购买详情列:商品/市场/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 + 操作 */
|
||||||
const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [
|
const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [
|
||||||
{ title: '商品', dataIndex: 'product_name', width: 160, align: 'center' },
|
{ title: '商品', dataIndex: 'product_name', width: 160, align: 'center' },
|
||||||
|
{ title: '市场', dataIndex: 'market', width: 90, align: 'center', render: (v) => v || '-' },
|
||||||
{
|
{
|
||||||
title: '参考零售价',
|
title: '参考零售价',
|
||||||
key: 'retail_price',
|
key: 'retail_price',
|
||||||
@@ -654,28 +657,29 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0);
|
const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0);
|
||||||
return (
|
return (
|
||||||
<Table.Summary.Row>
|
<Table.Summary.Row>
|
||||||
<Table.Summary.Cell index={0} colSpan={5} align="center">
|
<Table.Summary.Cell index={0} colSpan={6} align="center">
|
||||||
<Text strong>合计</Text>
|
<Text strong>合计</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={5} align="center">
|
<Table.Summary.Cell index={6} align="center">
|
||||||
<Text strong>{totalQuantity}</Text>
|
<Text strong>{totalQuantity}</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={6} align="center">
|
<Table.Summary.Cell index={7} align="center">
|
||||||
<Text strong>{totalWeight.toFixed(3)}斤</Text>
|
<Text strong>{totalWeight.toFixed(3)}斤</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={7} align="center">
|
<Table.Summary.Cell index={8} align="center">
|
||||||
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
|
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={8} align="center">
|
<Table.Summary.Cell index={9} align="center">
|
||||||
<Text strong>-</Text>
|
<Text strong>-</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
</Table.Summary.Row>
|
</Table.Summary.Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 供应商采购明细列:品名/包规/单位/成本价/数量/重量/金额(成本口径) */
|
/** 供应商采购明细列:品名/市场/包规/单位/成本价/数量/重量/金额(成本口径) */
|
||||||
const supplierColumns: TableProps<IPurchaseSupplierItem>['columns'] = [
|
const supplierColumns: TableProps<IPurchaseSupplierItem>['columns'] = [
|
||||||
{ title: '品名', dataIndex: 'product_name', width: 180, align: 'center' },
|
{ title: '品名', dataIndex: 'product_name', width: 180, align: 'center' },
|
||||||
|
{ title: '市场', dataIndex: 'market', width: 100, align: 'center', render: (v) => v || '-' },
|
||||||
{ title: '包规', dataIndex: 'product_spec', width: 110, align: 'center', render: (v) => v || '-' },
|
{ title: '包规', dataIndex: 'product_spec', width: 110, align: 'center', render: (v) => v || '-' },
|
||||||
{ title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' },
|
{ title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' },
|
||||||
{
|
{
|
||||||
@@ -709,16 +713,16 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
const totalAmount = supplierItems.reduce((sum, row) => sum + Number(row.amount), 0);
|
const totalAmount = supplierItems.reduce((sum, row) => sum + Number(row.amount), 0);
|
||||||
return (
|
return (
|
||||||
<Table.Summary.Row>
|
<Table.Summary.Row>
|
||||||
<Table.Summary.Cell index={0} colSpan={4} align="center">
|
<Table.Summary.Cell index={0} colSpan={5} align="center">
|
||||||
<Text strong>合计</Text>
|
<Text strong>合计</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={4} align="center">
|
<Table.Summary.Cell index={5} align="center">
|
||||||
<Text strong>{totalQuantity}</Text>
|
<Text strong>{totalQuantity}</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={5} align="center">
|
<Table.Summary.Cell index={6} align="center">
|
||||||
<Text strong>{totalWeight.toFixed(3)}斤</Text>
|
<Text strong>{totalWeight.toFixed(3)}斤</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell index={6} align="center">
|
<Table.Summary.Cell index={7} align="center">
|
||||||
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
|
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
</Table.Summary.Row>
|
</Table.Summary.Row>
|
||||||
|
|||||||
Reference in New Issue
Block a user