Compare commits

..

5 Commits

Author SHA1 Message Date
xinadmin 52ab195432 数据库迁移修改 2026-08-20 14:09:20 +08:00
xinadmin c1d490134e 小程序用户改用门店登录 2026-08-20 13:46:04 +08:00
xinadmin 6d9f02d831 采购金额 2026-08-17 18:33:36 +08:00
xinadmin 3dbb9205a4 采购单优化 2026-08-17 18:13:33 +08:00
xinadmin a06c45dc49 客户等级设置 2026-08-17 16:27:15 +08:00
276 changed files with 3433 additions and 4243 deletions
File diff suppressed because one or more lines are too long
+188 -101
View File
@@ -2,6 +2,7 @@
namespace App\Exports;
use App\Exceptions\RepositoryException;
use App\Models\ProductCategoryModel;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
@@ -10,44 +11,54 @@ use App\Models\StoreOrderItemModel;
use App\Models\SupplierModel;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 采购单导出
* 采购单商品明细导出:系统全部商品行(含本采购单无订货的商品,数量 0),
* 支持按供应商筛选;行尾合计 + 门店列合计;门店列整列填充突出颜色
*/
class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, WithStyles
class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, WithStyles
{
/** @var array<int, string> 商品ID => 顶级分类名 */
private array $categoryNames = [];
/** 门店列填充色(浅橙,突出显示) */
private const string STORE_FILL = 'FFFFF7E6';
/** @var array<int, string> 供应商ID => 名称 */
private array $supplierNames = [];
private ?Collection $rows = null;
/** @var array<int, string> 特殊行索引(1 起)=> 类型(title/scope/header/summary */
private array $specialRows = [];
/** 列头所在行索引 */
private int $headerRow = 1;
/** 数据末行索引 */
private int $lastRow = 1;
/** @var array<int, string> 门店ID => 名称(导出列) */
private array $storeNames = [];
private ?Collection $items = null;
/**
* @param PurchaseOrderModel $purchase 采购单
* @param int $supplierId 供应商筛选(0=全部)
*/
public function __construct(
private readonly PurchaseOrderModel $purchase,
private readonly int $supplierId = 0,
) {
}
/**
* 导出行(商品聚合行,按 分类sort → 商品sort 排序
* 导出行:标题/范围/列头/明细/合计全部手工构建(行位置不固定,不用 WithHeadings
*/
public function collection(): Collection
{
if ($this->items !== null) {
return $this->items;
if ($this->rows !== null) {
return $this->rows;
}
// 本采购单订货明细(关联订单过滤软删)
$orderItems = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $this->purchase->id)
@@ -61,135 +72,210 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping,
->pluck('name', 'id')
->toArray();
$products = ProductModel::withTrashed()
->with('category:id,sort')
->whereIn('id', $orderItems->pluck('product_id')->unique())
->get()
->keyBy('id');
$rows = [];
// 明细按商品聚合(快照字段取首条;有订货的商品以快照供应商为准)
$itemGroups = [];
foreach ($orderItems->groupBy('product_id') as $productId => $group) {
$first = $group->first();
$product = $products->get((int) $productId);
$unitCost = $first->cost_price ?? '0';
$quantity = '0';
$weight = '0';
$amount = '0';
$storeQuantities = array_fill_keys(array_keys($this->storeNames), '0');
$storeQuantities = array_fill_keys(array_keys($this->storeNames), 0);
foreach ($group as $item) {
$quantity = bcadd($quantity, (string) $item->quantity, 2);
$weight = bcadd($weight, (string) $item->weight, 3);
// 金额 = 数量 × 每包成本价(单价/包规不参与金额计算)
$amount = bcadd($amount, bcmul((string) $item->quantity, (string) $item->cost_price, 2), 2);
$storeQuantities[(int) $item->store_id] = bcadd(
$storeQuantities[(int) $item->store_id] ?? '0',
(string) $item->quantity,
2
);
$storeQuantities[(int) $item->store_id] = ($storeQuantities[(int) $item->store_id] ?? 0) + (int) $item->quantity;
}
$itemGroups[(int) $productId] = [
'snapshot' => $first,
'quantity' => $quantity,
'weight' => $weight,
'amount' => $amount,
'store_quantities' => $storeQuantities,
];
}
// 导出范围:系统全部上架商品 ∪ 本采购单有订货的商品(含已删/下架)
$products = ProductModel::withTrashed()
->with('category:id,sort')
->where('status', ProductModel::STATUS_ON)
->orWhereIn('id', array_keys($itemGroups))
->get()
->keyBy('id');
$categoryNames = $this->rootCategoryNames($products->pluck('category_id', 'id')->all());
$supplierNames = SupplierModel::withTrashed()->pluck('name', 'id')->toArray();
// 组装商品行(供应商筛选:有订货按快照 supplier_id,无订货按档案 supplier_id
$items = [];
foreach ($products as $productId => $product) {
$group = $itemGroups[(int) $productId] ?? null;
$snapshot = $group['snapshot'] ?? null;
$rowSupplierId = (int) ($snapshot->supplier_id ?? $product->supplier_id);
if ($this->supplierId > 0 && $rowSupplierId !== $this->supplierId) {
continue;
}
$rows[] = [
$quantity = $group['quantity'] ?? '0';
$amount = $group['amount'] ?? '0';
$spec = (string) ($snapshot->product_spec ?? $product->spec);
$items[] = [
'product_id' => (int) $productId,
'supplier_id' => (int) $first->supplier_id,
'product_name' => $first->product_name,
'product_spec' => $first->product_spec,
'unit' => $first->unit,
'cost_price' => (float) ($first->cost_price ?? 0),
'unit_cost' => (float) $unitCost,
'category' => $categoryNames[(int) $productId] ?? '',
'product_name' => (string) ($snapshot->product_name ?? $product->name),
'supplier' => $supplierNames[$rowSupplierId] ?? '',
'product_spec' => $spec,
'unit' => (string) ($snapshot->unit ?? $product->unit),
'cost_price' => (float) ($snapshot->cost_price ?? $product->cost_price),
// 参考零售价 = 加权平均售价 ÷ 包规数值(无订货行无售价数据,留空)
'retail_price' => $group !== null && (float) $quantity > 0
? $this->unitRefPrice((float) bcdiv($amount, $quantity, 4), $spec)
: null,
'quantity' => (float) $quantity,
'weight' => (float) $weight,
'weight' => (float) ($group['weight'] ?? '0'),
'amount' => (float) $amount,
'store_quantities' => array_map('floatval', $storeQuantities),
'store_quantities' => $group['store_quantities']
?? array_fill_keys(array_keys($this->storeNames), 0),
'category_sort' => (int) ($product->category->sort ?? 9999),
'product_sort' => (int) ($product->sort ?? 9999),
];
}
usort($rows, static fn (array $a, array $b): int =>
usort($items, static fn (array $a, array $b): int =>
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
$items = collect($rows);
$this->loadLookups($items);
if ($items === []) {
throw new RepositoryException('该供应商在系统中无商品行,无法导出');
}
$sort = 1;
return $this->items = $items->map(static function (array $row) use (&$sort): array {
$row['sort'] = $sort++;
return $row;
})->values();
}
// ===== 手工建行 =====
$rows = [];
$rowIndex = 0;
public function headings(): array
{
$this->collection();
// 标题行
$rows[] = ['采购单 ' . $this->purchase->purchase_no . '(采购日期 ' . $this->purchase->purchase_date->toDateString() . ''];
$this->specialRows[++$rowIndex] = 'title';
return array_merge(
['序号', '分类', '品名', '供应商', '包规', '单位', '成本', '单价', '数量', '实际称重', '金额'],
// 范围行:供应商筛选 / 导出时间
$scopeSupplier = '全部';
if ($this->supplierId > 0) {
$scopeSupplier = $supplierNames[$this->supplierId] ?? ('供应商#' . $this->supplierId);
}
$rows[] = ['供应商:' . $scopeSupplier . ' 导出时间:' . now()->format('Y-m-d H:i:s')];
$this->specialRows[++$rowIndex] = 'scope';
// 空行
$rows[] = [''];
$rowIndex++;
// 列头
$rows[] = array_merge(
['序号', '分类', '品名', '供应商', '包规', '单位', '成本', '参考零售价', '数量', '实际称重', '金额'],
array_values($this->storeNames),
);
}
$this->specialRows[++$rowIndex] = 'header';
$this->headerRow = $rowIndex;
public function map($row): array
{
return array_merge([
$row['sort'],
$this->categoryNames[$row['product_id']] ?? '',
$row['product_name'],
$this->supplierNames[$row['supplier_id']] ?? '',
$row['product_spec'],
$row['unit'],
$row['cost_price'],
$row['unit_cost'],
$row['quantity'],
$row['weight'],
$row['amount'],
], array_values($row['store_quantities']));
// 明细行
$totalQuantity = '0';
$totalWeight = '0';
$totalAmount = '0';
$storeTotals = array_fill_keys(array_keys($this->storeNames), 0);
foreach (array_values($items) as $sort => $item) {
$rows[] = array_merge([
$sort + 1,
$item['category'],
$item['product_name'],
$item['supplier'],
$item['product_spec'],
$item['unit'],
$item['cost_price'],
$item['retail_price'] !== null ? $item['retail_price'] : '',
$item['quantity'],
$item['weight'],
$item['amount'],
], array_values($item['store_quantities']));
$rowIndex++;
$totalQuantity = bcadd($totalQuantity, (string) $item['quantity'], 2);
$totalWeight = bcadd($totalWeight, (string) $item['weight'], 3);
$totalAmount = bcadd($totalAmount, (string) $item['amount'], 2);
foreach ($item['store_quantities'] as $storeId => $qty) {
$storeTotals[$storeId] += $qty;
}
}
// 合计行(行合计 + 门店列合计)
$rows[] = array_merge(
['', '', '合计', '', '', '', '', '', (float) $totalQuantity, (float) $totalWeight, (float) $totalAmount],
array_values($storeTotals),
);
$this->specialRows[++$rowIndex] = 'summary';
$this->lastRow = $rowIndex;
return $this->rows = collect($rows);
}
/**
* 表头加粗 + 冻结首行
* 标题/列头/合计加粗,门店列整列填充突出颜色,冻结列头
*/
public function styles(Worksheet $sheet): array
{
$sheet->freezePane('A2');
$this->collection();
return [
1 => ['font' => ['bold' => true]],
];
$sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [6, 10, 20, 12, 12, 8, 10, 12, 10, 12, 12];
foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
}
// 门店列整列(列头 → 合计行)填充突出颜色
$storeCount = count($this->storeNames);
for ($i = 0; $i < $storeCount; $i++) {
$column = Coordinate::stringFromColumnIndex(12 + $i);
$sheet->getColumnDimension($column)->setWidth(12);
$sheet->getStyle($column . $this->headerRow . ':' . $column . $this->lastRow)
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->getStartColor()
->setARGB(self::STORE_FILL);
}
$styles = [];
foreach ($this->specialRows as $row => $type) {
$style = match ($type) {
'title' => ['font' => ['bold' => true, 'size' => 14]],
'header', 'summary' => ['font' => ['bold' => true]],
default => [],
};
if ($style !== []) {
$styles[$row] = $style;
}
}
return $styles;
}
/**
* PDF 模板视图数据
* 每单位参考价 = 整单价 ÷ 包规数值(包规解析不出正数时按 1 处理;仅展示参考)
*/
private function unitRefPrice(float $price, string $spec): float
{
$pack = (float) preg_replace('/[^0-9.].*$/', '', $spec);
return $pack > 0 ? round($price / $pack, 2) : round($price, 2);
}
/**
* 商品ID => 顶级分类名(沿 parent_id 上溯取根分类)
*
* @return array{purchase: PurchaseOrderModel, items: Collection, storeNames: array<int, string>, categoryNames: array<int, string>, supplierNames: array<int, string>}
* @param array<int, int> $productCategoryIds 商品ID => 分类ID
* @return array<int, string>
*/
public function viewData(): array
private function rootCategoryNames(array $productCategoryIds): array
{
return [
'purchase' => $this->purchase,
'items' => $this->collection(),
'storeNames' => $this->storeNames,
'categoryNames' => $this->categoryNames,
'supplierNames' => $this->supplierNames,
];
}
/**
* 预加载供应商名与商品顶级分类名(商品/供应商均含软删除,保证历史单据可导出)
*/
private function loadLookups(Collection $items): void
{
$this->supplierNames = SupplierModel::withTrashed()
->whereIn('id', $items->pluck('supplier_id')->unique())
->pluck('name', 'id')
->toArray();
$productCategoryIds = ProductModel::withTrashed()
->whereIn('id', $items->pluck('product_id')->unique())
->pluck('category_id', 'id');
$categories = ProductCategoryModel::all()->keyBy('id');
$this->categoryNames = [];
$names = [];
foreach ($productCategoryIds as $productId => $categoryId) {
$rootName = '';
$cursor = (int) $categoryId;
@@ -202,7 +288,8 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping,
$rootName = $category->name;
$cursor = (int) $category->parent_id;
}
$this->categoryNames[(int) $productId] = $rootName;
$names[(int) $productId] = $rootName;
}
return $names;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Exports;
use App\Exceptions\RepositoryException;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
/**
* 门店购买详情导出:多门店合并为一个 XLSX(每门店一个工作表,工作表名=门店名称);
* 构造传入 storeId 时仅导出该门店(单工作表)
*/
class PurchaseStoreExport implements WithMultipleSheets
{
/**
* @param PurchaseOrderModel $purchase 采购单
* @param int $storeId 单门店导出(0=全部门店)
*/
public function __construct(
private readonly PurchaseOrderModel $purchase,
private readonly int $storeId = 0,
) {
}
/**
* @return array<int, PurchaseStoreSheet>
*/
public function sheets(): array
{
// 本采购单内有明细的门店(含软删除,保证历史单据可导出)
$query = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $this->purchase->id)
->whereNull('store_order.deleted_at');
if ($this->storeId > 0) {
$query->where('store_order_item.store_id', $this->storeId);
}
$storeIds = $query->distinct()->pluck('store_order_item.store_id');
if ($storeIds->isEmpty()) {
throw new RepositoryException(
$this->storeId > 0 ? '该门店在此采购单中无采购商品' : '该采购单无门店采购明细,无法导出'
);
}
$stores = StoreModel::withTrashed()
->whereIn('id', $storeIds)
->orderBy('id')
->get(['id', 'name']);
$usedNames = [];
$sheets = [];
foreach ($stores as $store) {
$sheets[] = new PurchaseStoreSheet(
$this->purchase,
$store,
SheetName::make((string) $store->name, (int) $store->id, $usedNames),
);
}
return $sheets;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
namespace App\Exports;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Services\PurchaseItemService;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithTitle;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 门店购买详情导出 · 单门店工作表
*/
class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, WithStyles, WithTitle
{
private ?Collection $rows = null;
/** @var array<int, string> 特殊行索引(1 起)=> 类型(title/header/summary */
private array $specialRows = [];
/** 列头所在行索引 */
private int $headerRow = 1;
public function __construct(
private readonly PurchaseOrderModel $purchase,
private readonly StoreModel $store,
private readonly string $sheetTitle,
) {
}
/**
* 导出行:标题/空行/列头/明细/合计
*/
public function collection(): Collection
{
if ($this->rows !== null) {
return $this->rows;
}
$items = app(PurchaseItemService::class)->storeSummaryRows($this->purchase->id, $this->store->id);
$rows = [];
$rowIndex = 0;
// 标题行
$rows[] = [$this->store->name . ' · 采购单 ' . $this->purchase->purchase_no];
$this->specialRows[++$rowIndex] = 'title';
// 空行
$rows[] = [''];
$rowIndex++;
// 列头
$rows[] = ['序号', '品名', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额'];
$this->specialRows[++$rowIndex] = 'header';
$this->headerRow = $rowIndex;
// 明细行
$totalQuantity = 0;
$totalWeight = '0';
$totalAmount = '0';
foreach (array_values($items) as $sort => $item) {
$rows[] = [
$sort + 1,
$item['product_name'],
$item['product_spec'],
$item['unit'],
(float) $item['price'],
(int) $item['quantity'],
(float) $item['weight'],
(float) $item['amount'],
];
$rowIndex++;
$totalQuantity += (int) $item['quantity'];
$totalWeight = bcadd($totalWeight, (string) $item['weight'], 3);
$totalAmount = bcadd($totalAmount, (string) $item['amount'], 2);
}
// 合计行
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
$this->specialRows[++$rowIndex] = 'summary';
return $this->rows = collect($rows);
}
public function title(): string
{
return $this->sheetTitle;
}
/**
* 标题/列头/合计加粗,冻结列头
*/
public function styles(Worksheet $sheet): array
{
$this->collection();
$sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [6, 24, 14, 8, 10, 10, 12, 12];
foreach ($widths as $index => $width) {
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($index + 1))
->setWidth($width);
}
$styles = [];
foreach ($this->specialRows as $row => $type) {
$style = match ($type) {
'title' => ['font' => ['bold' => true, 'size' => 14]],
'header', 'summary' => ['font' => ['bold' => true]],
default => [],
};
if ($style !== []) {
$styles[$row] = $style;
}
}
return $styles;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Exports;
use App\Exceptions\RepositoryException;
use App\Models\PurchaseOrderModel;
use App\Models\StoreOrderItemModel;
use App\Models\SupplierModel;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
/**
* 供应商采购明细导出:多供应商合并为一个 XLSX(每供应商一个工作表,工作表名=供应商名称);
* 构造传入 supplierId 时仅导出该供应商(单工作表)
*/
class PurchaseSupplierExport implements WithMultipleSheets
{
/**
* @param PurchaseOrderModel $purchase 采购单
* @param int $supplierId 单供应商导出(0=全部供应商)
*/
public function __construct(
private readonly PurchaseOrderModel $purchase,
private readonly int $supplierId = 0,
) {
}
/**
* @return array<int, PurchaseSupplierSheet>
*/
public function sheets(): array
{
// 本采购单内有明细的供应商(按订货明细快照 supplier_id 归集)
$query = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $this->purchase->id)
->whereNull('store_order.deleted_at')
->where('store_order_item.supplier_id', '>', 0);
if ($this->supplierId > 0) {
$query->where('store_order_item.supplier_id', $this->supplierId);
}
$supplierIds = $query->distinct()->pluck('store_order_item.supplier_id');
if ($supplierIds->isEmpty()) {
throw new RepositoryException(
$this->supplierId > 0 ? '该供应商在此采购单中无采购明细' : '该采购单无供应商采购明细,无法导出'
);
}
$suppliers = SupplierModel::withTrashed()
->whereIn('id', $supplierIds)
->orderBy('id')
->get(['id', 'name']);
$usedNames = [];
$sheets = [];
foreach ($suppliers as $supplier) {
$sheets[] = new PurchaseSupplierSheet(
$this->purchase,
$supplier,
SheetName::make((string) $supplier->name, (int) $supplier->id, $usedNames),
);
}
return $sheets;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
namespace App\Exports;
use App\Models\PurchaseOrderModel;
use App\Models\SupplierModel;
use App\Services\PurchaseItemService;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithTitle;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 供应商采购明细导出 · 单供应商工作表(成本口径:金额=Σ数量×成本价)
*/
class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison, WithStyles, WithTitle
{
private ?Collection $rows = null;
/** @var array<int, string> 特殊行索引(1 起)=> 类型(title/header/summary */
private array $specialRows = [];
/** 列头所在行索引 */
private int $headerRow = 1;
public function __construct(
private readonly PurchaseOrderModel $purchase,
private readonly SupplierModel $supplier,
private readonly string $sheetTitle,
) {
}
/**
* 导出行:标题/空行/列头/明细/合计
*/
public function collection(): Collection
{
if ($this->rows !== null) {
return $this->rows;
}
$items = app(PurchaseItemService::class)->supplierRows($this->purchase->id, $this->supplier->id);
$rows = [];
$rowIndex = 0;
// 标题行
$rows[] = [$this->supplier->name . ' · 采购单 ' . $this->purchase->purchase_no];
$this->specialRows[++$rowIndex] = 'title';
// 空行
$rows[] = [''];
$rowIndex++;
// 列头
$rows[] = ['序号', '品名', '包规', '单位', '成本价', '数量', '重量(斤)', '金额'];
$this->specialRows[++$rowIndex] = 'header';
$this->headerRow = $rowIndex;
// 明细行
$totalQuantity = 0;
$totalWeight = '0';
$totalAmount = '0';
foreach (array_values($items) as $sort => $item) {
$rows[] = [
$sort + 1,
$item['product_name'],
$item['product_spec'],
$item['unit'],
(float) $item['cost_price'],
(int) $item['quantity'],
(float) $item['weight'],
(float) $item['amount'],
];
$rowIndex++;
$totalQuantity += (int) $item['quantity'];
$totalWeight = bcadd($totalWeight, (string) $item['weight'], 3);
$totalAmount = bcadd($totalAmount, (string) $item['amount'], 2);
}
// 合计行
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
$this->specialRows[++$rowIndex] = 'summary';
return $this->rows = collect($rows);
}
public function title(): string
{
return $this->sheetTitle;
}
/**
* 标题/列头/合计加粗,冻结列头
*/
public function styles(Worksheet $sheet): array
{
$this->collection();
$sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [6, 24, 14, 8, 10, 10, 12, 12];
foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
}
$styles = [];
foreach ($this->specialRows as $row => $type) {
$style = match ($type) {
'title' => ['font' => ['bold' => true, 'size' => 14]],
'header', 'summary' => ['font' => ['bold' => true]],
default => [],
};
if ($style !== []) {
$styles[$row] = $style;
}
}
return $styles;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Exports;
/**
* Excel 工作表名合法化:剥离非法字符、31 字截断、重名追加 #id
*/
final class SheetName
{
/**
* @param string $name 原始名称(门店名/供应商名)
* @param int $id 实体ID(重名时追加)
* @param array<int, string> $used 已用名列表(引用传入,调用方维护)
*/
public static function make(string $name, int $id, array &$used): string
{
$base = str_replace(['[', ']', ':', '*', '?', '/', '\\'], '', $name);
$base = mb_substr($base === '' ? '未命名' : $base, 0, 28);
$title = $base;
if (in_array($title, $used, true)) {
$title = mb_substr($base, 0, 25) . '#' . $id;
}
$used[] = $title;
return $title;
}
}
@@ -5,9 +5,12 @@ namespace App\Http\Controllers\Customer;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Customer\CustomerLevelFormRequest;
use App\Models\CustomerLevelModel;
use App\Models\NoticeModel;
use App\Models\StoreModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
@@ -17,7 +20,7 @@ use Modules\Common\Http\Controllers\BaseController;
use Modules\SystemTool\Services\SysFileService;
/**
* 客户等级管理(同一商品按客户等级定价
* 客户等级管理(同一商品按等级上浮比例定价:售价 = 成本价 × (100 + percent) / 100
*/
#[RequestAttribute('/customer/level', 'customer.level')]
class CustomerLevelController extends BaseController
@@ -63,7 +66,7 @@ class CustomerLevelController extends BaseController
return $this->success();
}
/** 编辑等级 */
/** 编辑等级(上浮比例变更时通知该等级下门店用户) */
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, CustomerLevelFormRequest $request): JsonResponse
{
@@ -71,7 +74,14 @@ class CustomerLevelController extends BaseController
if (empty($model)) {
throw new RepositoryException('客户等级不存在');
}
$model->update($request->validated());
$validated = $request->validated();
DB::transaction(function () use ($model, $validated) {
$model->update($validated);
// 上浮比例变化 → 该等级下所有商品的售价联动变化,通知受影响门店用户
if ($model->wasChanged('percent')) {
$this->notifyPriceChange($model);
}
});
return $this->success();
}
@@ -86,9 +96,6 @@ class CustomerLevelController extends BaseController
if ($model->stores()->exists()) {
throw new RepositoryException('该等级下存在门店,无法删除');
}
if ($model->prices()->exists()) {
throw new RepositoryException('该等级下存在商品价格,无法删除');
}
$model->delete();
return $this->success();
}
@@ -100,8 +107,36 @@ class CustomerLevelController extends BaseController
$data = CustomerLevelModel::query()
->where('status', CustomerLevelModel::STATUS_NORMAL)
->orderBy('sort')
->get(['id', 'name'])
->get(['id', 'name', 'percent'])
->toArray();
return $this->success($data);
}
/**
* 上浮比例变更后,给该等级下全部正常门店生成价格变更通知
*/
private function notifyPriceChange(CustomerLevelModel $level): void
{
$storeIds = StoreModel::query()
->where('status', StoreModel::STATUS_NORMAL)
->where('level_id', $level->id)
->pluck('id');
$content = mb_substr(
'您所在客户等级「' . $level->name . '」的价格上浮比例已调整为 ' . (float) $level->percent . '%,商品价格将按新比例显示',
0,
500
);
foreach ($storeIds as $storeId) {
NoticeModel::create([
'store_id' => $storeId,
'type' => NoticeModel::TYPE_PRICE,
'title' => '商品价格变更',
'content' => $content,
'data' => ['level_id' => $level->id],
'is_read' => NoticeModel::UNREAD,
]);
}
}
}
@@ -1,80 +0,0 @@
<?php
namespace App\Http\Controllers\Customer;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Customer\MiniUserBindRequest;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 小程序用户管理(用户由小程序登录自动生成,后台只做绑定与状态管理,无增删)
*/
#[RequestAttribute('/customer/miniUser', 'customer.miniUser')]
class MiniUserController extends BaseController
{
protected array $searchField = [
'store_id' => '=',
'status' => '=',
];
protected array $quickSearchField = ['nickname', 'phone'];
/** 小程序用户列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch($params, UserModel::query()->with('store:id,name'))
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
return $this->success($data);
}
/**
* 绑定门店(一个门店可绑多个账号,一个账号只绑一个门店)
*/
#[PutRoute(route: '/{id}/bind', authorize: 'bind', where: ['id' => '[0-9]+'])]
public function bind(int $id, MiniUserBindRequest $request): JsonResponse
{
$validated = $request->validated();
$user = UserModel::find($id);
if (empty($user)) {
throw new RepositoryException('用户不存在');
}
$store = StoreModel::find((int) $validated['store_id']);
if (empty($store)) {
throw new RepositoryException('门店不存在');
}
$user->store_id = $store->id;
$user->save();
return $this->success();
}
/** 启用/停用 */
#[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])]
public function status(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'status' => 'required|integer|in:0,1',
], [
'status.required' => '状态不能为空',
'status.in' => '状态值不正确',
]);
$user = UserModel::find($id);
if (empty($user)) {
throw new RepositoryException('用户不存在');
}
$user->status = (int) $data['status'];
$user->save();
return $this->success();
}
}
@@ -14,7 +14,7 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 通知管理(小程序端消息;user_id=0 为全员广播)
* 通知管理(小程序端消息;store_id=0 为全员广播)
*/
#[RequestAttribute('/customer/notice', 'customer.notice')]
class NoticeController extends BaseController
@@ -22,7 +22,7 @@ class NoticeController extends BaseController
protected array $searchField = [
'type' => '=',
'is_read' => '=',
'user_id' => '=',
'store_id' => '=',
];
protected array $quickSearchField = ['title', 'content'];
@@ -40,7 +40,7 @@ class NoticeController extends BaseController
return $this->success($data);
}
/** 发布通知(user_id=0 全员广播) */
/** 发布通知(store_id=0 全员广播) */
#[PostRoute(authorize: 'create')]
public function create(NoticeFormRequest $request): JsonResponse
{
@@ -15,7 +15,7 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 门店管理(小程序下单主体,即客户)
* 门店管理(小程序下单主体,即客户;门店账号即小程序登录账号
*/
#[RequestAttribute('/customer/store', 'customer.store')]
class StoreController extends BaseController
@@ -23,13 +23,14 @@ class StoreController extends BaseController
protected array $searchField = [
'name' => 'like',
'code' => 'like',
'username' => 'like',
'level_id' => '=',
'status' => '=',
];
protected array $quickSearchField = ['name', 'code', 'contact', 'phone'];
protected array $quickSearchField = ['name', 'code', 'username', 'contact', 'phone'];
/** 门店列表(含等级名回显) */
/** 门店列表(含等级名回显;密码永不回显 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
@@ -42,17 +43,18 @@ class StoreController extends BaseController
return $this->success($data);
}
/** 创建门店 */
/** 创建门店(同时设置小程序登录账号与初始密码) */
#[PostRoute(authorize: 'create')]
public function create(StoreFormRequest $request): JsonResponse
{
$validated = $request->validated();
$validated['code'] = generate_unique_code(StoreModel::class);
$validated['password'] = password_hash((string) $validated['password'], PASSWORD_DEFAULT);
StoreModel::create($validated);
return $this->success();
}
/** 编辑门店 */
/** 编辑门店(密码留空则不修改) */
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, StoreFormRequest $request): JsonResponse
{
@@ -60,7 +62,13 @@ class StoreController extends BaseController
if (empty($model)) {
throw new RepositoryException('门店不存在');
}
$model->update($request->validated());
$validated = $request->validated();
if (empty($validated['password'])) {
unset($validated['password']);
} else {
$validated['password'] = password_hash((string) $validated['password'], PASSWORD_DEFAULT);
}
$model->update($validated);
return $this->success();
}
@@ -11,7 +11,6 @@ use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
@@ -349,9 +348,9 @@ class DashboardController extends BaseController
}
/**
* 基础档案计数:在营门店 / 在售商品 / 合作供应商 / 小程序用户
* 基础档案计数:在营门店 / 在售商品 / 合作供应商
*
* @return array{stores: int, products: int, suppliers: int, users: int}
* @return array{stores: int, products: int, suppliers: int}
*/
private function archives(): array
{
@@ -359,7 +358,6 @@ class DashboardController extends BaseController
'stores' => StoreModel::query()->where('status', StoreModel::STATUS_NORMAL)->count(),
'products' => ProductModel::query()->where('status', ProductModel::STATUS_ON)->count(),
'suppliers' => SupplierModel::query()->where('status', SupplierModel::STATUS_NORMAL)->count(),
'users' => UserModel::query()->where('status', UserModel::STATUS_NORMAL)->count(),
];
}
+1 -39
View File
@@ -2,13 +2,8 @@
namespace App\Http\Controllers;
use App\Http\Requests\UserRegisterRequest;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Trait\RequestJson;
@@ -17,7 +12,7 @@ class IndexController
{
use RequestJson;
// 权限验证白名单
protected array $noPermission = ['index', 'login', 'register', 'mail'];
protected array $noPermission = ['index'];
/** 获取首页信息 */
#[GetRoute('/index')]
@@ -27,37 +22,4 @@ class IndexController
return $this->success(compact('web_setting'));
}
/** 用户登录 */
#[PostRoute('/login')]
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'username' => 'required|min:4|alphaDash',
'password' => 'required|min:4|alphaDash',
]);
if (Auth::guard('users')->attempt($credentials, true)) {
$data = $request->user('users')
->createToken($credentials['username'])
->toArray();
return $this->success($data, __('user.login_success'));
}
return $this->error(__('user.login_error'));
}
/** 用户注册 */
#[PostRoute('/register')]
public function register(UserRegisterRequest $request): JsonResponse
{
$data = $request->validated();
$model = new UserModel;
$model->username = $data['username'];
$model->password = password_hash($data['password'], PASSWORD_DEFAULT);
$model->email = $data['email'];
if ($model->save()) {
return $this->success();
}
return $this->error('创建用户失败');
}
}
+43 -80
View File
@@ -3,10 +3,7 @@
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Http\Requests\UserUpdateInfoRequest;
use App\Models\StoreModel;
use App\Models\UserModel;
use App\Services\WechatService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
@@ -15,113 +12,79 @@ use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序认证
* 小程序认证(门店 账号 + 密码 登录,不再使用微信能力)
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class AuthController extends BaseMiniController
{
/** 小程序登录 */
/** 门店登录(账号 + 密码) */
#[PostRoute('/auth/login', authorize: false)]
public function login(Request $request): JsonResponse
{
$validated = $request->validate([
'code' => 'required|string'
'username' => 'required|string|max:20',
'password' => 'required|string|max:20',
], [
'code.required' => '登录参数格式错误',
'code.string' => '登录参数格式错误',
'username.required' => '请输入登录账号',
'password.required' => '请输入登录密码',
]);
$session = app(WechatService::class)->code2Session($validated['code']);
$store = StoreModel::where('username', $validated['username'])->first();
$user = UserModel::where('openid', $session['openid'])->first();
if ($user === null || $user->status === UserModel::STATUS_DISABLED) {
return $this->error('账号不存在或已被停用');
if ($store === null || ! password_verify($validated['password'], (string) $store->password)) {
return $this->error('账号或密码错误');
}
if ($store->status === StoreModel::STATUS_DISABLED) {
return $this->error('账号已被停用,请联系客服处理');
}
$user->last_login_at = date('Y-m-d H:i:s');
$user->save();
$token = $user->createToken($user->openid)->toArray();
return $this->success([
'token' => $token['plainTextToken'],
'user' => $user->toArray(),
], __('user.login_success'));
$store->last_login_at = date('Y-m-d H:i:s');
$store->save();
}
/** 小程序注册 */
#[PostRoute('/auth/register', authorize: false)]
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'code' => 'required|string',
'phoneCode' => 'required|string',
'storeCode' => 'required|string'
], [
'code.required' => '注册参数格式错误',
'code.string' => '注册参数格式错误',
'phoneCode.required' => '注册参数格式错误',
'phoneCode.string' => '注册参数格式错误',
'storeCode.required' => '门店编码必须填写',
'storeCode.string' => '注册参数格式错误',
]);
$store = StoreModel::where('code', $validated['storeCode'])->first();
if (!$store) {
return $this->error('门店不存在!');
}
// 通过 code 换取 openid、session_key、unionid
$session = app(WechatService::class)->code2Session($validated['code']);
$user = UserModel::where('openid', $session['openid'])->first();
if ($user) {
return $this->error('你的微信已经注册,请直接登录!');
}
$userData = [
'openid' => $session['openid'],
'unionid' => $session['unionid'] ?? '',
'username' => 'wx_'.uniqid(),
'nickname' => '微信用户',
'store_id' => $store->id,
'avatar' => '',
'password' => '',
'last_login_at' => date('Y-m-d H:i:s'),
];
$phone = app(WechatService::class)->getPhone($validated['phoneCode']);
$userData['phone'] = $phone ?? '';
$user = UserModel::create($userData);
$token = $user->createToken($user->username)->toArray();
$token = $store->createToken($store->username)->toArray();
$store->load('level:id,name');
return $this->success([
'token' => $token['plainTextToken'],
'user' => $user->toArray(),
'user' => $store->toArray(),
], __('user.login_success'));
}
/** 当前用户信息 */
/** 当前门店信息(含客户等级) */
#[GetRoute('/auth/info')]
public function info(Request $request): JsonResponse
{
$user = UserModel::with(['store.level:id,name'])
->find($request->user()->id);
if ($user === null) {
$store = StoreModel::with('level:id,name')->find($request->user()->id);
if ($store === null) {
throw new RepositoryException('账号不存在');
}
return $this->success($user->toArray());
return $this->success($store->toArray());
}
#[PutRoute('auth/info')]
public function setUserInfo(UserUpdateInfoRequest $request): JsonResponse
/** 修改登录密码 */
#[PutRoute('/auth/password')]
public function setPassword(Request $request): JsonResponse
{
UserModel::where('user_id', auth('user')->id())->update($request->validated());
$data = $request->validate([
'oldPassword' => 'required|string|max:20',
'newPassword' => 'required|string|min:6|max:20',
'rePassword' => 'required|same:newPassword',
], [
'oldPassword.required' => '请输入原密码',
'newPassword.required' => '请输入新密码',
'newPassword.min' => '新密码至少 6 位',
'rePassword.same' => '两次输入的密码不一致',
]);
return $this->error('更新成功');
$store = $this->currentStore($request);
if (! password_verify($data['oldPassword'], (string) $store->password)) {
return $this->error('原密码不正确');
}
$store->password = password_hash($data['newPassword'], PASSWORD_DEFAULT);
$store->save();
return $this->success([], '密码修改成功');
}
}
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\Request;
use Modules\Common\Http\Controllers\BaseController;
use Modules\SystemUser\Models\SysAccessToken;
@@ -13,71 +12,39 @@ use Modules\SystemUser\Models\SysAccessToken;
* 小程序端控制器基类
*
* 无 #[RequestAttribute],不会被 AnnoRoute 注册为路由。
* 提供当前用户获取与门店绑定前置校验。
* 门店即用户:登录主体就是门店(store 表),提供当前门店获取与状态校验。
*/
abstract class BaseMiniController extends BaseController
{
/**
* 当前小程序用户auth:sanctum 注入的 tokenable
* 当前登录门店auth:sanctum 注入的 tokenable
*/
protected function currentUser(Request $request): UserModel
protected function currentStore(Request $request): StoreModel
{
$user = UserModel::find($request->user()->id);
if ($user === null || $user->status === UserModel::STATUS_DISABLED) {
$store = StoreModel::find($request->user()->id);
if ($store === null || $store->status === StoreModel::STATUS_DISABLED) {
throw new RepositoryException('账号不存在或已被停用');
}
return $user;
return $store;
}
/**
* 可选登录场景手动识别当前用户(路由关闭 authorize 时使用)
* 可选登录场景手动识别当前门店(路由关闭 authorize 时使用)
*
* 手动解析 Bearer token;未携带 token、token 无效或非小程序用户时返回 null(不抛错)。
* 手动解析 Bearer token;未携带 token、token 无效或非门店账号时返回 null(不抛错)。
*/
protected function optionalUser(Request $request): ?UserModel
protected function optionalStore(Request $request): ?StoreModel
{
$token = $request->bearerToken();
if (empty($token)) {
return null;
}
$accessToken = SysAccessToken::findToken($token);
if ($accessToken === null || $accessToken->tokenable_type !== UserModel::class) {
if ($accessToken === null || $accessToken->tokenable_type !== StoreModel::class) {
return null;
}
$user = UserModel::find($accessToken->tokenable_id);
if ($user === null || $user->status === UserModel::STATUS_DISABLED) {
return null;
}
return $user;
}
/**
* 门店端前置校验
*/
protected function ensureStoreBound(UserModel $user): StoreModel
{
if ($user->store_id <= 0) {
throw new RepositoryException('尚未绑定门店,请联系客服处理');
}
$store = StoreModel::find($user->store_id);
if ($store === null || $store->status !== StoreModel::STATUS_NORMAL) {
throw new RepositoryException('门店不存在或已停用,请联系客服处理');
}
return $store;
}
/**
* 用户当前绑定的正常门店(未绑定/已停用返回 null,不抛错)
*/
protected function boundStore(UserModel $user): ?StoreModel
{
if ($user->store_id <= 0) {
return null;
}
$store = StoreModel::find($user->store_id);
if ($store === null || $store->status !== StoreModel::STATUS_NORMAL) {
$store = StoreModel::find($accessToken->tokenable_id);
if ($store === null || $store->status === StoreModel::STATUS_DISABLED) {
return null;
}
+3 -6
View File
@@ -43,8 +43,7 @@ class BillController extends BaseMiniController
'pageSize.max' => '每页数量最大 50',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$query = BillModel::query()
->where('store_id', $store->id)
@@ -100,8 +99,7 @@ class BillController extends BaseMiniController
'category_id' => 'nullable|integer|min:0',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string) $data['ids'])))));
if ($ids === [] || count($ids) > 100) {
@@ -131,8 +129,7 @@ class BillController extends BaseMiniController
#[GetRoute('/bill/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$bill = BillModel::with('purchase:id,purchase_no,purchase_date')
->where('store_id', $store->id)
+19 -41
View File
@@ -5,8 +5,8 @@ namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Mini\MiniCartRequest;
use App\Models\CartModel;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -34,9 +34,8 @@ class CartController extends BaseMiniController
#[PostRoute('/cart')]
public function store(MiniCartRequest $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
if ($store->level_id <= 0) {
$store = $this->currentStore($request);
if ($store->level_id <= 0 || $store->level === null) {
return $this->error('门店未设置客户等级,无法加购,请联系客服');
}
@@ -45,20 +44,12 @@ class CartController extends BaseMiniController
if ($product === null) {
return $this->error('商品不存在或已下架,请刷新后重试');
}
// 存在性校验与计价类型无关(百分比行 price 可能为 0 也能加购)
$hasPrice = ProductPriceModel::query()
->where('product_id', $productId)
->where('level_id', $store->level_id)
->exists();
if (! $hasPrice) {
return $this->error('商品「' . $product->name . '」价格未设置,无法加购');
}
$quantity = (string) $request->validated('quantity');
$cart = DB::transaction(function () use ($user, $productId, $quantity) {
$cart = DB::transaction(function () use ($store, $productId, $quantity) {
$row = CartModel::query()
->where('user_id', $user->id)
->where('store_id', $store->id)
->where('product_id', $productId)
->lockForUpdate()
->first();
@@ -75,7 +66,7 @@ class CartController extends BaseMiniController
}
return CartModel::create([
'user_id' => $user->id,
'store_id' => $store->id,
'product_id' => $productId,
'quantity' => $quantity,
]);
@@ -93,11 +84,10 @@ class CartController extends BaseMiniController
#[GetRoute('/cart', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$rows = CartModel::query()
->where('user_id', $user->id)
->where('store_id', $store->id)
->orderBy('id', 'desc')
->get();
@@ -115,11 +105,8 @@ class CartController extends BaseMiniController
->unique()->values()->all();
$products = ProductModel::withTrashed()->whereIn('id', $productIds)->get()->keyBy('id');
$priceRows = ProductPriceModel::query()
->where('level_id', $store->level_id)
->whereIn('product_id', $productIds)
->get(['product_id', 'price', 'price_type', 'percent'])
->keyBy('product_id');
// 门店等级(售价 = 成本价 × (100 + 等级上浮比例) / 100
$level = $store->level_id > 0 ? $store->level : null;
// 图片一次查回(避免 ProductModel::$appends images_arr 的 N+1)。
// 注意:image_ids 有 imageIds Attribute 访问器(get 返回数组),需取原始值
@@ -141,16 +128,10 @@ class CartController extends BaseMiniController
foreach ($rows as $row) {
$product = $products->get($row->product_id);
$productOn = $product !== null && $product->status === ProductModel::STATUS_ON;
// 实际价(百分比计价行按成本价上浮换算);未设等级为 null
$priceRow = $priceRows->get($row->product_id);
$price = $priceRow === null
// 实际价(按门店等级上浮比例换算);未设等级为 null
$price = $level === null
? null
: ProductPriceModel::calcActualPrice(
(int) $priceRow->price_type,
$priceRow->price,
$priceRow->percent,
(float) ($product?->cost_price ?? 0),
);
: CustomerLevelModel::calcLevelPrice((float) ($product?->cost_price ?? 0), $level->percent);
$buyable = $productOn && $price !== null;
$quantity = (string) $row->quantity;
@@ -191,12 +172,11 @@ class CartController extends BaseMiniController
#[PutRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function update(int $id, MiniCartRequest $request): JsonResponse
{
$user = $this->currentUser($request);
$this->ensureStoreBound($user);
$store = $this->currentStore($request);
$row = CartModel::query()
->where('id', $id)
->where('user_id', $user->id)
->where('store_id', $store->id)
->first();
if ($row === null) {
throw new RepositoryException('购物车项不存在');
@@ -214,12 +194,11 @@ class CartController extends BaseMiniController
#[DeleteRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function destroy(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$this->ensureStoreBound($user);
$store = $this->currentStore($request);
$deleted = CartModel::query()
->where('id', $id)
->where('user_id', $user->id)
->where('store_id', $store->id)
->delete();
if ($deleted === 0) {
throw new RepositoryException('购物车项不存在');
@@ -234,10 +213,9 @@ class CartController extends BaseMiniController
#[DeleteRoute('/cart', authorize: true)]
public function clear(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$this->ensureStoreBound($user);
$store = $this->currentStore($request);
CartModel::query()->where('user_id', $user->id)->delete();
CartModel::query()->where('store_id', $store->id)->delete();
return $this->success([], '购物车已清空');
}
+16 -16
View File
@@ -11,34 +11,34 @@ use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序通知(本通知 + 全员广播)
* 小程序通知(本通知 + 全员广播)
*
* 广播已读处理:user_id=0 的广播是全局共享记录,直接改 is_read 会影响其他用户
* 因此标记已读时复制一条本专属的已读记录(data.broadcast_from 记来源),
* 广播已读处理:store_id=0 的广播是全局共享记录,直接改 is_read 会影响其他门店
* 因此标记已读时复制一条本专属的已读记录(data.broadcast_from 记来源),
* 列表查询时排除已有已读副本的广播。
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class NoticeController extends BaseMiniController
{
/** 本通知 + 全员广播(user_id in [0, 当前id]),分页 + unread_count */
/** 本通知 + 全员广播(store_id in [0, 当前id]),分页 + unread_count */
#[GetRoute('/notice', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->currentStore($request);
// 本已读过的广播来源ID(已读副本记录)
// 本已读过的广播来源ID(已读副本记录)
$readBroadcastIds = NoticeModel::query()
->where('user_id', $user->id)
->where('store_id', $store->id)
->whereNotNull('data->broadcast_from')
->pluck('data')
->map(static fn (?array $data) => $data['broadcast_from'] ?? null)
->filter()
->values();
$query = NoticeModel::query()->where(function ($q) use ($user, $readBroadcastIds) {
$q->where('user_id', $user->id)
$query = NoticeModel::query()->where(function ($q) use ($store, $readBroadcastIds) {
$q->where('store_id', $store->id)
->orWhere(function ($broadcastQuery) use ($readBroadcastIds) {
$broadcastQuery->where('user_id', NoticeModel::BROADCAST_USER_ID);
$broadcastQuery->where('store_id', NoticeModel::BROADCAST_STORE_ID);
if ($readBroadcastIds->isNotEmpty()) {
$broadcastQuery->whereNotIn('id', $readBroadcastIds->all());
}
@@ -55,24 +55,24 @@ class NoticeController extends BaseMiniController
return $this->success($data);
}
/** 标记已读(广播 → 复制本已读副本;个人通知 → 直接更新) */
/** 标记已读(广播 → 复制本已读副本;定向通知 → 直接更新) */
#[PutRoute('/notice/{id}/read', authorize: true, where: ['id' => '[0-9]+'])]
public function read(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->currentStore($request);
$notice = NoticeModel::whereIn('user_id', [NoticeModel::BROADCAST_USER_ID, $user->id])->find($id);
$notice = NoticeModel::whereIn('store_id', [NoticeModel::BROADCAST_STORE_ID, $store->id])->find($id);
if ($notice === null) {
throw new RepositoryException('通知不存在');
}
if ($notice->user_id === NoticeModel::BROADCAST_USER_ID) {
$exists = NoticeModel::where('user_id', $user->id)
if ($notice->store_id === NoticeModel::BROADCAST_STORE_ID) {
$exists = NoticeModel::where('store_id', $store->id)
->where('data->broadcast_from', $notice->id)
->exists();
if (! $exists) {
NoticeModel::create([
'user_id' => $user->id,
'store_id' => $store->id,
'type' => $notice->type,
'title' => $notice->title,
'content' => $notice->content,
+11 -30
View File
@@ -4,8 +4,8 @@ namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Mini\MiniOrderRequest;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Services\BillNumberService;
@@ -30,16 +30,16 @@ class OrderController extends BaseMiniController
#[PostRoute('/order', authorize: true)]
public function store(MiniOrderRequest $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
if ($store->level_id <= 0) {
$store = $this->currentStore($request);
$level = $store->level_id > 0 ? $store->level : null;
if ($level === null) {
throw new RepositoryException('门店未设置客户等级,无法下单,请联系客服');
}
$items = $request->validated('items');
$remark = (string) ($request->validated('remark') ?? '');
$order = DB::transaction(function () use ($store, $items, $remark) {
$order = DB::transaction(function () use ($store, $level, $items, $remark) {
$productIds = array_map(static fn ($row) => (int) $row['product_id'], $items);
$products = ProductModel::query()
@@ -48,12 +48,6 @@ class OrderController extends BaseMiniController
->get()
->keyBy('id');
$priceRows = ProductPriceModel::query()
->where('level_id', $store->level_id)
->whereIn('product_id', $productIds)
->get(['product_id', 'price', 'price_type', 'percent'])
->keyBy('product_id');
$totalQuantity = '0';
$totalAmount = '0';
$now = now();
@@ -64,18 +58,9 @@ class OrderController extends BaseMiniController
if ($product === null) {
throw new RepositoryException('存在已下架或不存在的商品,请刷新后重试');
}
if (! isset($priceRows[$productId])) {
throw new RepositoryException('商品「' . $product->name . '」未设置您所在等级的价格,无法下单');
}
// 实际价(百分比计价行按成本价上浮换算,$products 已含 cost_price
$priceRow = $priceRows[$productId];
$price = ProductPriceModel::calcActualPrice(
(int) $priceRow->price_type,
$priceRow->price,
$priceRow->percent,
$product->cost_price,
);
// 实际价(按门店等级上浮比例换算,$products 已含 cost_price
$price = CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent);
$quantity = (string) $row['quantity'];
$amount = bcmul($price, $quantity, 2);
$totalQuantity = bcadd($totalQuantity, $quantity, 2);
@@ -152,8 +137,7 @@ class OrderController extends BaseMiniController
'pageSize.max' => '每页数量最大 50',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$query = StoreOrderModel::query()
->where('store_id', $store->id)
@@ -234,8 +218,7 @@ class OrderController extends BaseMiniController
throw new RepositoryException('period 参数只能是 day/week/month');
}
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
// 按数据库方言选择周期分组表达式(生产 MySQL / 测试 SQLite
$driver = DB::connection()->getDriverName();
@@ -273,8 +256,7 @@ class OrderController extends BaseMiniController
#[GetRoute('/order/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$order = StoreOrderModel::with('items')
->where('store_id', $store->id)
@@ -290,8 +272,7 @@ class OrderController extends BaseMiniController
#[PutRoute('/order/{id}/cancel', authorize: true, where: ['id' => '[0-9]+'])]
public function cancel(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$order = StoreOrderModel::where('store_id', $store->id)->find($id);
if ($order === null) {
@@ -48,8 +48,7 @@ class PaymentController extends BaseMiniController
#[GetRoute('/payment', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$query = PaymentModel::query()
->where('store_id', $store->id)
@@ -89,11 +88,10 @@ class PaymentController extends BaseMiniController
'remark.max' => '备注超过最大长度',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$billIds = array_map('intval', $data['bill_ids']);
$payment = DB::transaction(function () use ($store, $user, $data, $billIds) {
$payment = DB::transaction(function () use ($store, $data, $billIds) {
$bills = BillModel::query()
->where('store_id', $store->id)
->whereIn('id', $billIds)
@@ -119,7 +117,6 @@ class PaymentController extends BaseMiniController
$payment = PaymentModel::create([
'payment_no' => app(BillNumberService::class)->make('ZF'),
'store_id' => $store->id,
'user_id' => $user->id,
'amount' => $amount,
'pay_method' => (int) $data['pay_method'],
'voucher_ids' => array_map('intval', $data['voucher_ids']),
@@ -144,8 +141,7 @@ class PaymentController extends BaseMiniController
#[GetRoute('/payment/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$payment = PaymentModel::query()
->where('store_id', $store->id)
+13 -37
View File
@@ -3,9 +3,9 @@
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Models\CustomerLevelModel;
use App\Models\ProductCategoryModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
@@ -50,30 +50,16 @@ class ProductController extends BaseMiniController
->orderBy('id')
->paginate($pageSize);
// 当前门店的等级价格(一次性取出,避免逐行查询
$user = $this->optionalUser($request);
$store = $user !== null ? $this->boundStore($user) : null;
$priceRows = collect();
if ($store !== null && $store->level_id > 0) {
$priceRows = ProductPriceModel::query()
->where('level_id', $store->level_id)
->whereIn('product_id', $paginator->getCollection()->pluck('id'))
->get(['product_id', 'price', 'price_type', 'percent'])
->keyBy('product_id');
}
// 当前门店的等级(售价 = 成本价 × (100 + 等级上浮比例) / 100
$store = $this->optionalStore($request);
$level = ($store !== null && $store->level_id > 0) ? $store->level : null;
$paginator->getCollection()->transform(
static function (ProductModel $product) use ($priceRows): array {
static function (ProductModel $product) use ($level): array {
$row = $product->toArray();
// 实际价(百分比计价行按成本价上浮换算;成本价不随序列化输出)
$priceRow = $priceRows->get($product->id);
$row['price'] = $priceRow !== null
? ProductPriceModel::calcActualPrice(
(int) $priceRow->price_type,
(string) $priceRow->price,
(string) $priceRow->percent,
(string) $product->cost_price,
)
// 实际价(按等级上浮比例换算;成本价不随序列化输出)
$row['price'] = $level !== null
? CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent)
: null;
return $row;
}
@@ -83,7 +69,7 @@ class ProductController extends BaseMiniController
}
/**
* 商品详情(免登录浏览;登录门店按等级显示换算价,未登录/未绑店/未设等级 price=null
* 商品详情(免登录浏览;登录门店按等级上浮比例显示换算价,未登录/未设等级 price=null
*/
#[GetRoute('/product/{id}', authorize: false, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
@@ -97,20 +83,10 @@ class ProductController extends BaseMiniController
}
$price = null;
$user = $this->optionalUser($request);
$store = $user !== null ? $this->boundStore($user) : null;
if ($store !== null && $store->level_id > 0) {
$priceRow = ProductPriceModel::query()
->forProductLevel($product->id, $store->level_id)
->first(['price', 'price_type', 'percent']);
if ($priceRow !== null) {
$price = ProductPriceModel::calcActualPrice(
(int) $priceRow->price_type,
(string) $priceRow->price,
(string) $priceRow->percent,
(string) $product->cost_price,
);
}
$store = $this->optionalStore($request);
$level = ($store !== null && $store->level_id > 0) ? $store->level : null;
if ($level !== null) {
$price = CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent);
}
$data = $product->toArray();
@@ -18,8 +18,7 @@ class StoreController extends BaseMiniController
#[GetRoute('/store/info', authorize: true)]
public function info(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
return $this->success([
'id' => $store->id,
@@ -46,8 +45,7 @@ class StoreController extends BaseMiniController
'address.max' => '地址最长 255 个字符',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$store->update($data);
@@ -26,9 +26,9 @@ class UploadController extends BaseMiniController
'file.max' => '图片不能超过 5MB',
]);
$user = $this->currentUser($request);
$store = $this->currentStore($request);
// 分组 4=用户上传,渠道 20=APP用户
$result = app(SysFileService::class)->upload($data['file'], 4, 20, $user->id);
$result = app(SysFileService::class)->upload($data['file'], 4, 20, $store->id);
return $this->success([
'id' => $result['id'],
@@ -5,8 +5,8 @@ namespace App\Http\Controllers\Order;
use App\Exceptions\RepositoryException;
use App\Models\BillModel;
use App\Models\NoticeModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
use App\Services\ItemImageResolver;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -219,26 +219,27 @@ class StoreOrderController extends BaseController
}
/**
* 状态流转后通知门店用户
* 状态流转后通知门店
*/
private function notifyStore(StoreOrderModel $order): void
{
$userIds = UserModel::query()
->where('store_id', $order->store_id)
->where('status', UserModel::STATUS_NORMAL)
->pluck('id');
$store = StoreModel::query()
->where('id', $order->store_id)
->where('status', StoreModel::STATUS_NORMAL)
->first();
if ($store === null) {
return;
}
$statusName = StoreOrderModel::STATUS_NAMES[$order->status] ?? (string) $order->status;
foreach ($userIds as $userId) {
NoticeModel::create([
'user_id' => $userId,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => mb_substr("您的订单 {$order->order_no} 状态已更新为「{$statusName}", 0, 500),
'data' => ['order_id' => $order->id],
'is_read' => NoticeModel::UNREAD,
]);
}
NoticeModel::create([
'store_id' => $store->id,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => mb_substr("您的订单 {$order->order_no} 状态更新为「{$statusName}", 0, 500),
'data' => ['order_id' => $order->id],
'is_read' => NoticeModel::UNREAD,
]);
}
/**
@@ -8,9 +8,7 @@ use App\Http\Requests\Product\ProductFormRequest;
use App\Models\CustomerLevelModel;
use App\Models\NoticeModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -24,7 +22,7 @@ use Modules\Common\Http\Controllers\BaseController;
use Modules\SystemTool\Services\SysFileService;
/**
* 商品档案管理
* 商品档案管理(售价 = 成本价 × (100 + 客户等级上浮比例) / 100,不再维护等级价格行)
*/
#[RequestAttribute('/product/goods', 'product.goods')]
class ProductController extends BaseController
@@ -38,7 +36,7 @@ class ProductController extends BaseController
protected array $quickSearchField = ['name', 'spec'];
/** A1 商品列表(含分类/供应商/各等级价格cost_price 在 $hidden 中,后台列表需显式恢复) */
/** A1 商品列表(含分类/供应商;prices 为按启用等级上浮比例换算的展示价cost_price 在 $hidden 中,后台列表需显式恢复) */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
@@ -46,12 +44,26 @@ class ProductController extends BaseController
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch(
$params,
ProductModel::query()->with(['category:id,name', 'supplier:id,name', 'prices.level:id,name'])
ProductModel::query()->with(['category:id,name', 'supplier:id,name'])
)
->orderBy('sort')
->orderBy('id', 'desc')
->paginate($pageSize);
$data->getCollection()->makeVisible('cost_price');
// 按启用等级直接换算展示价(无等级价格表,价格由成本价 × 等级上浮比例得出)
$levels = $this->enabledLevels();
$data->getCollection()->transform(static function (ProductModel $product) use ($levels) {
$row = $product->toArray();
$row['prices'] = $levels->map(static fn (CustomerLevelModel $level) => [
'level_id' => $level->id,
'level' => ['id' => $level->id, 'name' => $level->name],
'percent' => (float) $level->percent,
'price' => CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent),
])->values()->all();
return $row;
});
return $this->success($data->toArray());
}
@@ -70,32 +82,15 @@ class ProductController extends BaseController
}
/** 创建商品(事务内建商品 + 同步等级价格) */
/** 创建商品 */
#[PostRoute(authorize: 'create')]
public function create(ProductFormRequest $request): JsonResponse
{
$validated = $request->validated();
$prices = $validated['prices'] ?? [];
unset($validated['prices']);
$product = DB::transaction(function () use ($validated, $prices) {
$product = ProductModel::create($validated);
foreach ($prices as $row) {
ProductPriceModel::create([
'product_id' => $product->id,
'level_id' => (int) $row['level_id'],
'price' => $row['price'],
'price_type' => (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED),
'percent' => $row['percent'] ?? 0,
]);
}
return $product;
});
$product = ProductModel::create($request->validated());
return $this->success(['id' => $product->id]);
}
/** 编辑商品prices 按 level_id upsert,删除已移除的等级行;未提交 prices 键时保持原价) */
/** 编辑商品 */
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, ProductFormRequest $request): JsonResponse
{
@@ -103,34 +98,11 @@ class ProductController extends BaseController
if (empty($product)) {
throw new RepositoryException('商品不存在');
}
$validated = $request->validated();
$prices = $validated['prices'] ?? [];
unset($validated['prices']);
DB::transaction(function () use ($product, $validated, $prices, $request) {
$product->update($validated);
if ($request->has('prices')) {
$levelIds = [];
foreach ($prices as $row) {
$levelId = (int) $row['level_id'];
$levelIds[] = $levelId;
ProductPriceModel::updateOrCreate(
['product_id' => $product->id, 'level_id' => $levelId],
[
'price' => $row['price'],
'price_type' => (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED),
'percent' => $row['percent'] ?? 0,
],
);
}
$product->prices()->whereNotIn('level_id', $levelIds)->delete();
}
});
$product->update($request->validated());
return $this->success();
}
/** 删除商品(软删除,连带价格行一并删除 */
/** 删除商品(软删除) */
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
public function delete(int $id): JsonResponse
{
@@ -138,22 +110,18 @@ class ProductController extends BaseController
if (empty($product)) {
throw new RepositoryException('商品不存在');
}
DB::transaction(function () use ($product) {
$product->prices()->delete();
$product->delete();
});
$product->delete();
return $this->success();
}
/**
* A2 价格矩阵:行=商品(支持 category_id / keyword 过滤 + page/pageSize 服务端分页),
* 列=全部启用等级,值=实际销售价(缺失为 null);行内含 cost_price 与每等级
* price_type_{levelId} / percent_{levelId},供前端判断计价类型与联动重算
* 列=全部启用等级,值=按等级上浮比例换算的售价(成本价未设置为 null);仅成本价可编辑
*/
#[GetRoute('/priceMatrix', 'query')]
public function priceMatrix(Request $request): JsonResponse
{
$query = ProductModel::query()->with('prices:id,product_id,level_id,price,price_type,percent');
$query = ProductModel::query();
if (($categoryId = (int) $request->input('category_id', 0)) > 0) {
$query->where('category_id', $categoryId);
}
@@ -168,13 +136,9 @@ class ProductController extends BaseController
$page = max(1, (int) $request->input('page', 1));
$products = $query->orderBy('sort')->orderBy('id')->paginate($pageSize, ['*'], 'page', $page);
$levels = CustomerLevelModel::query()
->where('status', CustomerLevelModel::STATUS_NORMAL)
->orderBy('sort')
->get(['id', 'name']);
$levels = $this->enabledLevels();
$rows = $products->getCollection()->map(function (ProductModel $product) use ($levels) {
$priceMap = $product->prices->keyBy('level_id');
$rows = $products->getCollection()->map(static function (ProductModel $product) use ($levels) {
$row = [
'id' => $product->id,
'name' => $product->name,
@@ -183,31 +147,27 @@ class ProductController extends BaseController
'cost_price' => (float) $product->cost_price,
];
foreach ($levels as $level) {
$price = $priceMap[$level->id] ?? null;
$row['price_' . $level->id] = $price
? (float) ProductPriceModel::calcActualPrice(
(int) $price->price_type,
$price->price,
$price->percent,
$product->cost_price,
)
$row['price_' . $level->id] = $product->cost_price > 0
? (float) CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent)
: null;
$row['price_type_' . $level->id] = $price ? (int) $price->price_type : ProductPriceModel::PRICE_TYPE_FIXED;
$row['percent_' . $level->id] = $price ? (float) $price->percent : 0;
}
return $row;
});
return $this->success([
'levels' => $levels->toArray(),
'levels' => $levels->map(static fn (CustomerLevelModel $level) => [
'id' => $level->id,
'name' => $level->name,
'percent' => (float) $level->percent,
])->values()->all(),
'rows' => $rows->values()->toArray(),
'total' => $products->total(),
]);
}
/**
* A2 批量调价:三类更新行(成本价 / 固定价 / 成本百分比,可混合同一行),事务写入,
* 写完后给受影响门店生成 Noticetype=price
* A2 批量调价:批量调整成本价(等级售价随之按上浮比例联动),事务写入,
* 写完后给全部正常门店的用户生成 Noticetype=price
*/
#[PutRoute('/batchPrice', 'batchPrice')]
public function batchPrice(BatchPriceRequest $request): JsonResponse
@@ -216,65 +176,30 @@ class ProductController extends BaseController
DB::transaction(function () use ($updates) {
$productIds = [];
$levelIds = [];
foreach ($updates as $row) {
$productId = (int) $row['product_id'];
$productIds[$productId] = true;
// 分支1:成本价更新(百分比计价的基数,可与等级价行同在一行)
if (array_key_exists('cost_price', $row) && $row['cost_price'] !== null) {
ProductModel::whereKey($productId)->update(['cost_price' => $row['cost_price']]);
}
// 分支2/3:等级价格行(固定价或成本百分比,按 price_type 区分)
if (isset($row['level_id'])) {
$levelId = (int) $row['level_id'];
$levelIds[$levelId] = true;
ProductPriceModel::updateOrCreate(
['product_id' => $productId, 'level_id' => $levelId],
[
'price' => $row['price'] ?? 0,
'price_type' => (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED),
'percent' => $row['percent'] ?? 0,
],
);
}
ProductModel::whereKey($productId)->update(['cost_price' => $row['cost_price']]);
}
// 成本价变更只影响「该商品下百分比计价」的等级;受影响门店等级 = 等级更新行 ∪ 百分比行等级
$percentLevelIds = ProductPriceModel::query()
->whereIn('product_id', array_keys($productIds))
->where('price_type', ProductPriceModel::PRICE_TYPE_PERCENT)
->pluck('level_id')
->merge($levelIds)
->unique()
->all();
$productNames = ProductModel::whereIn('id', array_keys($productIds))
->pluck('name')
->implode('、');
$content = mb_substr('以下商品价格已调整:' . $productNames . ',下次登录小程序后按新价格显示', 0, 500);
// 受影响门店:客户等级在受影响等级范围内的正常门店,通知其绑定的正常用户
$userIds = UserModel::query()
->where('status', UserModel::STATUS_NORMAL)
->whereIn('store_id', function ($q) use ($percentLevelIds) {
$q->select('id')
->from('store')
->where('status', StoreModel::STATUS_NORMAL)
->whereIn('level_id', $percentLevelIds);
})
// 成本价变更影响所有等级的售价:通知全部正常门店
$storeIds = StoreModel::query()
->where('status', StoreModel::STATUS_NORMAL)
->pluck('id');
foreach ($userIds as $userId) {
foreach ($storeIds as $storeId) {
NoticeModel::create([
'user_id' => $userId,
'store_id' => $storeId,
'type' => NoticeModel::TYPE_PRICE,
'title' => '商品价格变更',
'content' => $content,
'data' => [
'product_ids' => array_keys($productIds),
'level_ids' => array_keys($levelIds),
],
'is_read' => NoticeModel::UNREAD,
]);
@@ -298,4 +223,17 @@ class ProductController extends BaseController
->toArray();
return $this->success($data);
}
/**
* 启用中的客户等级(列表/矩阵共用的等级列来源)
*
* @return \Illuminate\Database\Eloquent\Collection<int, CustomerLevelModel>
*/
private function enabledLevels(): \Illuminate\Database\Eloquent\Collection
{
return CustomerLevelModel::query()
->where('status', CustomerLevelModel::STATUS_NORMAL)
->orderBy('sort')
->get(['id', 'name', 'percent']);
}
}
@@ -4,22 +4,29 @@ namespace App\Http\Controllers\Purchase;
use App\Exceptions\RepositoryException;
use App\Exports\PurchaseOrderExport;
use App\Exports\PurchaseStoreExport;
use App\Exports\PurchaseSupplierExport;
use App\Http\Requests\Purchase\PurchaseBillGenerateRequest;
use App\Http\Requests\Purchase\PurchaseCellUpdateRequest;
use App\Http\Requests\Purchase\PurchaseRowUpdateRequest;
use App\Http\Requests\Purchase\PurchaseStoreItemRequest;
use App\Models\BillModel;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Services\BillGenerateService;
use App\Services\ItemImageResolver;
use App\Services\PurchaseGenerateService;
use App\Services\PurchaseItemService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
@@ -98,6 +105,8 @@ class PurchaseOrderController extends BaseController
$quantity = 0;
// 采购总重量
$weight = '0';
// 采购总金额(Σ明细 amount,参考零售价按 金额÷数量 加权)
$amount = '0';
// 门店明细
$cellsMap = [];
@@ -106,6 +115,7 @@ class PurchaseOrderController extends BaseController
// 累加总量
$quantity += (int) $item->quantity;
$weight = bcadd($weight, (string) $item->weight, 3);
$amount = bcadd($amount, (string) $item->amount, 2);
// 按门店合并
if (!isset($cellsMap[$storeId])) {
@@ -127,6 +137,7 @@ class PurchaseOrderController extends BaseController
'cost_price' => $first->cost_price, // 成本价
'quantity' => $quantity,
'weight' => (float) $weight,
'amount' => $amount,
'cells' => $cellsMap,
'category_sort' => (int) ($product->category->sort ?? 9999),
'product_sort' => (int) ($product->sort ?? 9999),
@@ -247,22 +258,68 @@ class PurchaseOrderController extends BaseController
}
/**
* 导出采购单 Excel 表格
* 导出采购单商品明细 Excel 表格(系统全部商品行,支持按供应商筛选)
*
* @throws
*/
#[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])]
public function export(int $id): Response
public function export(int $id, Request $request): Response
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$supplierId = (int) $request->query('supplier_id', 0);
return Excel::download(
new PurchaseOrderExport($purchase),
$purchase->purchase_no . '_采购单.xlsx',
);
$filename = $purchase->purchase_no . '_采购单.xlsx';
if ($supplierId > 0) {
$supplier = SupplierModel::withTrashed()->find($supplierId);
$filename = $purchase->purchase_no . '_采购单_' . ($supplier->name ?? ('供应商' . $supplierId)) . '.xlsx';
}
return Excel::download(new PurchaseOrderExport($purchase, $supplierId), $filename);
}
/**
* 导出门店购买详情(多工作表:每门店一个工作表;?store_id= 单门店导出)
*/
#[GetRoute(route: '/{id}/exportStores', authorize: 'export', where: ['id' => '[0-9]+'])]
public function exportStores(int $id, Request $request): Response
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$storeId = (int) $request->query('store_id', 0);
$filename = $purchase->purchase_no . '_门店购买详情.xlsx';
if ($storeId > 0) {
$store = StoreModel::withTrashed()->find($storeId);
$filename = $purchase->purchase_no . '_门店购买详情_' . ($store->name ?? ('门店' . $storeId)) . '.xlsx';
}
return Excel::download(new PurchaseStoreExport($purchase, $storeId), $filename);
}
/**
* 导出供应商采购明细(多工作表:每供应商一个工作表;?supplier_id= 单供应商导出)
*/
#[GetRoute(route: '/{id}/exportSuppliers', authorize: 'export', where: ['id' => '[0-9]+'])]
public function exportSuppliers(int $id, Request $request): Response
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$supplierId = (int) $request->query('supplier_id', 0);
$filename = $purchase->purchase_no . '_供应商采购明细.xlsx';
if ($supplierId > 0) {
$supplier = SupplierModel::withTrashed()->find($supplierId);
$filename = $purchase->purchase_no . '_供应商采购明细_' . ($supplier->name ?? ('供应商' . $supplierId)) . '.xlsx';
}
return Excel::download(new PurchaseSupplierExport($purchase, $supplierId), $filename);
}
/**
@@ -348,68 +405,191 @@ class PurchaseOrderController extends BaseController
throw new RepositoryException('采购单不存在');
}
$items = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $purchase->id)
->where('store_order_item.store_id', $storeId)
->whereNull('store_order.deleted_at')
->select('store_order_item.*')
->orderBy('store_order_item.id')
->get();
// 排序键:分类 sort → 商品 sort(与明细矩阵同序)
$products = ProductModel::withTrashed()
->with('category:id,sort')
->whereIn('id', $items->pluck('product_id')->unique())
->get()
->keyBy('id');
$rows = [];
foreach ($items->groupBy('product_id') as $productId => $group) {
$product = $products->get((int) $productId);
$first = $group->first();
// 采购总数量(包数)
$quantity = 0;
// 预计金额 = Σ 明细 amount
$amount = '0';
// 总重量
$weight = '0';
foreach ($group as $item) {
$weight = bcadd($weight, (string) $item->weight, 3);
$quantity += (int) $item->quantity;
$amount = bcadd($amount, (string) $item->amount, 2);
}
$rows[] = [
'product_id' => (int) $productId,
'product_name' => $first->product_name,
'product_spec' => $first->product_spec, // 包规
'unit' => $first->unit, // 单位
'price' => $quantity > 0 // 加权平均单价
? bcdiv($amount, (string) $quantity, 2)
: (string) $first->price,
'quantity' => $quantity,
'amount' => $amount,
'weight' => $weight,
'category_sort' => (int) ($product->category->sort ?? 9999),
'product_sort' => (int) ($product->sort ?? 9999),
];
}
usort($rows, static fn (array $a, array $b): int =>
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
$store = StoreModel::withTrashed()->find($storeId);
return $this->success([
'store' => $store ? ['id' => $store->id, 'name' => $store->name] : null,
'items' => array_map(static function (array $row): array {
unset($row['category_sort'], $row['product_sort']);
return $row;
}, $rows),
'items' => app(PurchaseItemService::class)->storeSummaryRows($purchase->id, $storeId),
]);
}
/**
* 门店购买详情:新增单品(挂靠该门店在采购单中的最新一笔订单,
* 单价按门店等级上浮比例换算,无等级按成本价兜底)
*
* @throws Throwable
*/
#[PostRoute(route: '/{id}/store/{storeId}/item', authorize: 'update', where: ['id' => '[0-9]+', 'storeId' => '[0-9]+'])]
public function storeItemStore(int $id, int $storeId, PurchaseStoreItemRequest $request): JsonResponse
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$this->assertPurchaseEditable($purchase);
$validated = $request->validated();
$productId = (int) $validated['product_id'];
$product = ProductModel::find($productId);
if ($product === null) {
throw new RepositoryException('商品不存在或已被删除,无法添加');
}
return DB::transaction(function () use ($purchase, $storeId, $product, $validated) {
// 该门店在采购单中的最新一笔订单
$order = StoreOrderModel::query()
->where('purchase_id', $purchase->id)
->where('store_id', $storeId)
->orderByDesc('id')
->lockForUpdate()
->first();
if ($order === null) {
throw new RepositoryException('该门店不在此采购单中,无法添加单品');
}
$duplicated = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('store_id', $storeId)
->where('product_id', $product->id)
->exists();
if ($duplicated) {
throw new RepositoryException('该商品已在此门店采购明细中,请直接修改数量');
}
// 单价:门店等级上浮换算价(无等级按成本价兜底)
$store = StoreModel::withTrashed()->find($storeId);
$level = ($store !== null && $store->level_id > 0) ? $store->level : null;
$price = $level !== null
? CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent)
: bcadd((string) $product->cost_price, '0', 2);
$quantity = (int) $validated['quantity'];
$item = StoreOrderItemModel::create([
'order_id' => $order->id,
'purchase_id' => $purchase->id,
'bill_id' => 0,
'store_id' => $storeId,
'product_id' => $product->id,
'category_id' => (int) $product->category_id,
'supplier_id' => (int) $product->supplier_id,
'product_name' => $product->name,
'product_spec' => $product->spec,
'unit' => (string) $product->unit,
'price' => $price,
'image_ids' => implode(',', (array) $product->image_ids),
'content' => (string) $product->content,
'shelf_life' => (int) $product->shelf_life,
'quantity' => $quantity,
'weight' => bcadd((string) ($validated['weight'] ?? 0), '0', 3),
'amount' => bcmul($price, (string) $quantity, 2),
'cost_price' => (string) $product->cost_price,
'remark' => '',
]);
$service = app(PurchaseItemService::class);
$service->recalcOrder($order->id);
$service->recalcPurchase($purchase->id);
return $this->success(['id' => $item->id], '已添加单品');
});
}
/**
* 门店购买详情:修改单品(数量/称重/单价);同门店同商品多笔订单明细时合并到最早一条
*
* @throws Throwable
*/
#[PutRoute(route: '/{id}/store/{storeId}/item/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'storeId' => '[0-9]+', 'productId' => '[0-9]+'])]
public function storeItemUpdate(int $id, int $storeId, int $productId, PurchaseStoreItemRequest $request): JsonResponse
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$this->assertPurchaseEditable($purchase);
$validated = $request->validated();
return DB::transaction(function () use ($purchase, $storeId, $productId, $validated) {
$items = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('store_id', $storeId)
->where('product_id', $productId)
->orderBy('id')
->lockForUpdate()
->get();
if ($items->isEmpty()) {
throw new RepositoryException('该门店在此采购单中无此商品明细');
}
$affectedOrderIds = $items->pluck('order_id')->unique()->all();
// 多笔订单明细合并到最早一条
$survivor = $items->first();
foreach ($items->skip(1) as $extra) {
$extra->delete();
}
$survivor->quantity = (int) $validated['quantity'];
if (isset($validated['price'])) {
$survivor->price = bcadd((string) $validated['price'], '0', 2);
}
if (isset($validated['weight'])) {
$survivor->weight = bcadd((string) $validated['weight'], '0', 3);
}
$survivor->amount = bcmul((string) $survivor->quantity, (string) $survivor->price, 2);
$survivor->save();
$service = app(PurchaseItemService::class);
foreach ($affectedOrderIds as $orderId) {
$service->recalcOrder((int) $orderId);
}
$service->recalcPurchase($purchase->id);
return $this->success([], '单品已更新');
});
}
/**
* 门店购买详情:移除单品(该门店此商品的全部订货明细一并删除)
*
* @throws Throwable
*/
#[DeleteRoute(route: '/{id}/store/{storeId}/item/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'storeId' => '[0-9]+', 'productId' => '[0-9]+'])]
public function storeItemDelete(int $id, int $storeId, int $productId): JsonResponse
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$this->assertPurchaseEditable($purchase);
DB::transaction(function () use ($purchase, $storeId, $productId) {
$items = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('store_id', $storeId)
->where('product_id', $productId)
->lockForUpdate()
->get();
if ($items->isEmpty()) {
throw new RepositoryException('该门店在此采购单中无此商品明细');
}
$affectedOrderIds = $items->pluck('order_id')->unique()->all();
foreach ($items as $item) {
$item->delete();
}
$service = app(PurchaseItemService::class);
foreach ($affectedOrderIds as $orderId) {
$service->recalcOrder((int) $orderId);
}
$service->recalcPurchase($purchase->id);
});
return $this->success([], '已移除单品');
}
/**
* 账单生成预览
*/
@@ -489,7 +669,9 @@ class PurchaseOrderController extends BaseController
}
/**
* 商品行修改:品名/供应商/包规/单位/成本
* 商品行修改:品名/供应商/包规/单位/成本(同步更新商品档案;商品已删除则跳过档案同步)
* 成本价变化时,按各门店客户等级的上浮比例重算明细单价与金额(无等级按成本价兜底),
* 并级联重算涉及订单与采购单汇总
* @throws Throwable
*/
#[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])]
@@ -499,31 +681,77 @@ class PurchaseOrderController extends BaseController
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
throw new RepositoryException('采购单已完成,不允许修改明细');
}
$this->assertPurchaseEditable($purchase);
$validated = $request->validated();
$items = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('product_id', $productId)
->lockForUpdate()
->get();
if ($items->isEmpty()) {
throw new RepositoryException('该采购单下无此商品的订货明细');
}
foreach ($items as $item) {
$item->fill($validated)->save();
}
return DB::transaction(function () use ($purchase, $productId, $validated) {
$items = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('product_id', $productId)
->lockForUpdate()
->get();
if ($items->isEmpty()) {
throw new RepositoryException('该采购单下无此商品的订货明细');
}
// 重算采购单成本
$query = StoreOrderItemModel::query()->where('purchase_id', $purchase->id);
$purchase->total_quantity = $query->sum('quantity');
$purchase->estimate_amount = $query->sum(DB::raw('quantity * cost_price'));
$purchase->save();
// 成本价是否变化(未变化时保留明细单价,避免覆盖单元格/单品的手工改价)
$newCost = bcadd((string) $validated['cost_price'], '0', 2);
$costChanged = bccomp($newCost, bcadd((string) $items->first()->cost_price, '0', 2), 2) !== 0;
return $this->success(['count' => $items->count()], '已同步 ' . $items->count() . ' 条订货明细');
// 成本变化 → 按门店等级上浮比例重算单价(一次性取回涉及门店的等级,避免逐行查询)
$levelPercents = [];
if ($costChanged) {
$levelPercents = StoreModel::withTrashed()
->with('level:id,percent')
->whereIn('id', $items->pluck('store_id')->unique())
->get(['id', 'level_id'])
->mapWithKeys(static fn (StoreModel $store) => [
$store->id => (float) ($store->level?->percent ?? 0),
])
->all();
}
foreach ($items as $item) {
$item->fill($validated);
if ($costChanged) {
$item->price = CustomerLevelModel::calcLevelPrice(
$newCost,
$levelPercents[(int) $item->store_id] ?? 0,
);
$item->amount = bcmul($item->price, (string) $item->quantity, 2);
}
$item->save();
}
// 同步保存到商品档案(软删除商品跳过,明细照常更新)
$product = ProductModel::find($productId);
$productSynced = $product !== null;
if ($productSynced) {
$product->update([
'name' => $validated['product_name'],
'supplier_id' => $validated['supplier_id'],
'spec' => $validated['product_spec'],
'unit' => $validated['unit'],
'cost_price' => $validated['cost_price'],
]);
}
$service = app(PurchaseItemService::class);
// 成本变化会引起明细金额变动,涉及订单需逐一重算
if ($costChanged) {
foreach ($items->pluck('order_id')->unique() as $orderId) {
$service->recalcOrder((int) $orderId);
}
}
$service->recalcPurchase($purchase->id);
$message = '已同步 ' . $items->count() . ' 条订货明细'
. ($productSynced ? '与商品档案' : ';商品已删除,档案未同步')
. ($costChanged ? ';门店单价已按等级上浮比例重算' : '');
return $this->success(['count' => $items->count()], $message);
});
}
/**
@@ -544,31 +772,31 @@ class PurchaseOrderController extends BaseController
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
throw new RepositoryException('采购单已完成,不允许修改明细');
}
$this->assertPurchaseEditable($purchase);
$item->quantity = $validated['quantity'];
$item->price = $validated['price'];
if (array_key_exists('weight', $validated)) {
$item->weight = bcadd((string) $validated['weight'], '0', 3);
}
$item->amount = bcmul($validated['quantity'], $validated['price'], 3);
$item->amount = bcmul((string) $validated['quantity'], (string) $validated['price'], 2);
$item->save();
// 重算订单金额
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
$order->total_weight = $order->items()->sum('weight');
$order->total_amount = $order->items()->sum('amount');
$order->save();
// 重算采购单重量
$purchase->total_weight = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->sum('weight');
$purchase->save();
$service = app(PurchaseItemService::class);
$service->recalcOrder((int) $item->order_id);
$service->recalcPurchase($purchase->id);
return $this->success();
});
}
/**
* 采购单编辑闸:仅进行中(待采购)允许修改明细
*/
private function assertPurchaseEditable(PurchaseOrderModel $purchase): void
{
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
throw new RepositoryException('采购单已完成,不允许修改明细');
}
}
}
@@ -35,7 +35,7 @@ class PaymentController extends BaseController
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch($params, PaymentModel::query()
->with(['store:id,name', 'user:id,nickname', 'auditor:id,nickname'])
->with(['store:id,name', 'auditor:id,nickname'])
->withCount('bills'))
->orderBy('status')
->orderBy('id', 'desc')
@@ -48,7 +48,7 @@ class PaymentController extends BaseController
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$payment = PaymentModel::with(['store:id,name,contact,phone', 'user:id,nickname', 'auditor:id,nickname'])->find($id);
$payment = PaymentModel::with(['store:id,name,contact,phone', 'auditor:id,nickname'])->find($id);
if (empty($payment)) {
throw new RepositoryException('支付记录不存在');
}
-68
View File
@@ -1,68 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserUpdateInfoRequest;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\Common\Trait\RequestJson;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
#[RequestAttribute('/api/user', authGuard: 'users')]
class UserController
{
use RequestJson;
protected array $noPermission = ['refreshToken'];
#[GetRoute]
public function getUserInfo(): JsonResponse
{
$info = auth()->user();
return $this->success(compact('info'));
}
#[PostRoute('/logout')]
public function logout(): JsonResponse
{
$user_id = auth('users')->id();
$model = new UserModel;
if ($model->logout($user_id)) {
return $this->success('退出登录成功');
} else {
return $this->error($model->getErrorMsg());
}
}
#[PutRoute]
public function setUserInfo(UserUpdateInfoRequest $request): JsonResponse
{
UserModel::where('user_id', auth('user')->id())->update($request->validated());
return $this->error('更新成功');
}
#[PostRoute('/setPwd')]
public function setPassword(Request $request): JsonResponse
{
$data = $request->validate([
'oldPassword' => 'required|string|max:20',
'newPassword' => 'required|string|min:6|max:20',
'rePassword' => 'required|same:newPassword',
]);
$user_id = auth('user')->id();
$user = UserModel::query()->find($user_id);
if (! password_verify($data['oldPassword'], $user['password'])) {
return $this->error('旧密码不正确!');
}
$user->password = password_hash($data['newPassword'], PASSWORD_DEFAULT);
if ($user->save()) {
return $this->success('更新成功');
}
return $this->error('更新失败');
}
}
@@ -23,6 +23,7 @@ class CustomerLevelFormRequest extends BaseFormRequest
return [
'name' => ['required', 'string', 'max:50', $unique],
'percent' => 'nullable|numeric|min:0|max:999.99',
'sort' => 'nullable|integer',
'status' => 'nullable|integer|in:0,1',
'icon_id' => ['nullable', 'integer', new Exists(SysFileModel::class, 'id')],
@@ -35,6 +36,9 @@ class CustomerLevelFormRequest extends BaseFormRequest
'name.required' => '等级名称不能为空',
'name.max' => '等级名称最长 50 个字符',
'name.unique' => '等级名称已存在',
'percent.numeric' => '价格上浮比例必须为数字',
'percent.min' => '价格上浮比例不能小于 0',
'percent.max' => '价格上浮比例不能超过 999.99',
'status.in' => '状态值不正确',
'icon_id.exists' => '请重新上传图片'
];
@@ -1,28 +0,0 @@
<?php
namespace App\Http\Requests\Customer;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 小程序用户绑定 验证(绑定门店)
*/
class MiniUserBindRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
public function rules(): array
{
return [
'store_id' => 'required|integer|min:1',
];
}
public function messages(): array
{
return [
'store_id.required' => '绑定门店时必须选择门店',
'store_id.min' => '门店ID不正确',
];
}
}
@@ -5,7 +5,7 @@ namespace App\Http\Requests\Customer;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 通知 创建 验证(user_id 留空 = 全员广播)
* 通知 创建 验证(store_id 留空 = 全员广播)
*/
class NoticeFormRequest extends BaseFormRequest
{
@@ -13,13 +13,13 @@ class NoticeFormRequest extends BaseFormRequest
protected function prepareForValidation(): void
{
$this->merge(['user_id' => (int) ($this->input('user_id') ?? 0)]);
$this->merge(['store_id' => (int) ($this->input('store_id') ?? 0)]);
}
public function rules(): array
{
return [
'user_id' => 'required|integer|min:0',
'store_id' => 'required|integer|min:0',
'type' => 'required|string|in:order,price,system',
'title' => 'required|string|max:100',
'content' => 'nullable|string|max:500',
@@ -6,7 +6,7 @@ use Illuminate\Validation\Rule;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 门店 创建/编辑 验证
* 门店 创建/编辑 验证(登录账号唯一;密码创建必填,编辑留空不修改)
*/
class StoreFormRequest extends BaseFormRequest
{
@@ -14,8 +14,19 @@ class StoreFormRequest extends BaseFormRequest
public function rules(): array
{
$id = (int) $this->route('id', 0);
return [
'name' => 'required|string|max:100',
'username' => [
'required',
'string',
'min:4',
'max:20',
'alpha_dash',
Rule::unique('store', 'username')->ignore($id),
],
'password' => ($this->isMethod('post') ? 'required' : 'nullable') . '|string|min:6|max:20',
'level_id' => 'required|integer|exists:customer_level,id',
'contact' => 'nullable|string|max:50',
'phone' => 'nullable|string|max:20',
@@ -31,6 +42,14 @@ class StoreFormRequest extends BaseFormRequest
return [
'name.required' => '门店名称不能为空',
'name.max' => '门店名称最长 100 个字符',
'username.required' => '登录账号不能为空',
'username.min' => '登录账号至少 4 个字符',
'username.max' => '登录账号最长 20 个字符',
'username.alpha_dash' => '登录账号只能由字母、数字、中划线、下划线组成',
'username.unique' => '登录账号已被使用',
'password.required' => '登录密码不能为空',
'password.min' => '登录密码至少 6 位',
'password.max' => '登录密码最长 20 位',
'level_id.required' => '请选择客户等级',
'level_id.exists' => '客户等级不存在',
'payment_cycle_days.integer' => '回款周期必须为整数',
@@ -2,18 +2,13 @@
namespace App\Http\Requests\Product;
use App\Models\ProductPriceModel;
use Closure;
use Illuminate\Validation\Validator;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 批量调价 验证(A2 价格矩阵编辑提交)
*
* updates 每行支持三类更新(可混合同一行)
* 等级售价 = 成本价 × (100 + 等级上浮比例) / 100,矩阵仅支持批量调整成本价
* 成本行 {product_id, cost_price}
* 固定价行 {product_id, level_id, price_type?:0, price}
* 百分比行 {product_id, level_id, price_type:1, percent}price 可选,等价固定价)
*/
class BatchPriceRequest extends BaseFormRequest
{
@@ -24,54 +19,10 @@ class BatchPriceRequest extends BaseFormRequest
return [
'updates' => 'required|array|min:1',
'updates.*.product_id' => 'required|integer|exists:product,id',
'updates.*.cost_price' => 'nullable|numeric|min:0|max:99999999',
'updates.*.level_id' => 'nullable|integer|exists:customer_level,id',
'updates.*.price_type' => 'nullable|integer|in:0,1',
'updates.*.price' => 'nullable|numeric|min:0|max:99999999',
'updates.*.percent' => 'nullable|numeric|min:0|max:999.99',
'updates.*.cost_price' => 'required|numeric|min:0|max:99999999',
];
}
/**
* 行级交叉校验(在 after() 内按实际数据逐行判断,避免 required_with* 通配符参数解析不可靠):
* - 每行必须至少包含成本价或等级价格更新
* - 出现等级字段(price/percent/price_type)时必须带 level_id
* - 等级行必须有 price percent
* - 成本百分比计价(price_type=1)时上浮百分点必填
*/
public function after(): Closure
{
return function (Validator $validator): void {
$data = (array) $validator->getData();
foreach ((array) ($data['updates'] ?? []) as $index => $row) {
$row = (array) $row;
$hasCost = array_key_exists('cost_price', $row) && $row['cost_price'] !== null && $row['cost_price'] !== '';
$hasLevel = isset($row['level_id']);
$hasPrice = array_key_exists('price', $row) && $row['price'] !== null && $row['price'] !== '';
$hasPercent = array_key_exists('percent', $row) && $row['percent'] !== null && $row['percent'] !== '';
$hasType = array_key_exists('price_type', $row);
if (! $hasCost && ! $hasLevel) {
$validator->errors()->add("updates.{$index}", '调价行缺少成本价或等级价格');
continue;
}
if (($hasPrice || $hasPercent || $hasType) && ! $hasLevel) {
$validator->errors()->add("updates.{$index}.level_id", '等级价格行缺少客户等级');
continue;
}
if ($hasLevel && ! $hasPrice && ! $hasPercent) {
$validator->errors()->add("updates.{$index}", '等级价格行缺少单价或上浮百分点');
continue;
}
$priceType = (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED);
if ($priceType === ProductPriceModel::PRICE_TYPE_PERCENT && ! $hasPercent) {
$validator->errors()->add("updates.{$index}.percent", '按成本百分比计价时必须填写上浮百分点');
}
}
};
}
public function messages(): array
{
return [
@@ -79,15 +30,9 @@ class BatchPriceRequest extends BaseFormRequest
'updates.min' => '请至少提交一条价格调整',
'updates.*.product_id.required' => '调价行缺少商品',
'updates.*.product_id.exists' => '商品不存在',
'updates.*.cost_price.required' => '调价行缺少成本价',
'updates.*.cost_price.numeric' => '成本价必须为数字',
'updates.*.cost_price.min' => '成本价不能小于 0',
'updates.*.level_id.exists' => '客户等级不存在',
'updates.*.price_type.in' => '计价类型不正确',
'updates.*.price.numeric' => '单价必须为数字',
'updates.*.price.min' => '单价不能小于 0',
'updates.*.percent.numeric' => '上浮百分点必须为数字',
'updates.*.percent.min' => '上浮百分点不能小于 0',
'updates.*.percent.max' => '上浮百分点不能超过 999.99',
];
}
}
@@ -3,7 +3,6 @@
namespace App\Http\Requests\Product;
use App\Models\ProductCategoryModel;
use App\Models\ProductPriceModel;
use App\Models\SupplierModel;
use Closure;
use Illuminate\Validation\Rules\Exists;
@@ -12,7 +11,7 @@ use Modules\Common\Http\Requests\BaseFormRequest;
use Modules\SystemTool\Models\SysFileModel;
/**
* 商品档案 创建/编辑 验证(含多等级价格 prices 数组
* 商品档案 创建/编辑 验证(售价按客户等级上浮比例换算,不再维护等级价格行
*/
class ProductFormRequest extends BaseFormRequest
{
@@ -35,16 +34,11 @@ class ProductFormRequest extends BaseFormRequest
'status' => 'nullable|integer|in:0,1',
'cost_price' => 'nullable|numeric|min:0|max:99999999',
'remark' => 'nullable|string|max:255',
'prices' => 'nullable|array',
'prices.*.level_id' => 'required|integer|exists:customer_level,id',
'prices.*.price_type' => 'nullable|integer|in:0,1',
'prices.*.price' => 'required|numeric|min:0|max:99999999',
'prices.*.percent' => 'nullable|numeric|min:0|max:999.99',
];
}
/**
* 交叉校验:商品只能挂在末级分类;按成本百分比计价(price_type=1)时上浮百分点必填
* 交叉校验:商品只能挂在末级分类
* Laravel 12 FormRequest after() 需返回单个 Closure,由容器 call 后注册到 Validator
*/
public function after(): Closure
@@ -55,13 +49,6 @@ class ProductFormRequest extends BaseFormRequest
if ($categoryId > 0 && ProductCategoryModel::where('parent_id', $categoryId)->exists()) {
$validator->errors()->add('category_id', '该分类下存在子分类,请选择末级分类');
}
foreach ((array) ($data['prices'] ?? []) as $index => $row) {
$priceType = (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED);
if ($priceType === ProductPriceModel::PRICE_TYPE_PERCENT
&& (! array_key_exists('percent', (array) $row) || $row['percent'] === null || $row['percent'] === '')) {
$validator->errors()->add("prices.{$index}.percent", '按成本百分比计价时必须填写上浮百分点');
}
}
};
}
@@ -74,17 +61,8 @@ class ProductFormRequest extends BaseFormRequest
'category_id.exists' => '商品分类不存在',
'supplier_id.exists' => '供应商不存在',
'status.in' => '状态值不正确',
'prices.*.level_id.required' => '价格行缺少客户等级',
'prices.*.level_id.exists' => '客户等级不存在',
'prices.*.price.required' => '价格行缺少单价',
'prices.*.price.numeric' => '单价必须为数字',
'prices.*.price.min' => '单价不能小于 0',
'cost_price.numeric' => '成本价必须为数字',
'cost_price.min' => '成本价不能小于 0',
'prices.*.price_type.in' => '计价类型不正确',
'prices.*.percent.numeric' => '上浮百分点必须为数字',
'prices.*.percent.min' => '上浮百分点不能小于 0',
'prices.*.percent.max' => '上浮百分点不能超过 999.99',
];
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests\Purchase;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 采购单门店单品 新增/修改 验证(amount = 数量×单价 由后端重算)
*/
class PurchaseStoreItemRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
public function rules(): array
{
if ($this->isUpdate()) {
return [
'quantity' => 'required|integer|min:0',
'price' => 'nullable|numeric|min:0',
'weight' => 'nullable|numeric|min:0',
];
}
return [
'product_id' => 'required|integer|exists:product,id',
'quantity' => 'required|integer|min:1',
'weight' => 'nullable|numeric|min:0',
];
}
public function messages(): array
{
return [
'product_id.required' => '请选择商品',
'product_id.exists' => '商品不存在或已删除',
'quantity.required' => '采购数量不能为空',
'quantity.integer' => '采购数量必须为整数',
'quantity.min' => '采购数量不能小于 0',
'price.numeric' => '单价必须为数字',
'price.min' => '单价不能小于 0',
'weight.numeric' => '称重必须为数字',
'weight.min' => '称重不能小于 0',
];
}
}
-18
View File
@@ -1,18 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserRegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'username' => 'required|min:4|alphaDash',
'password' => 'required|min:4|alphaDash',
'rePassword' => 'required|min:4|same:password',
'email' => 'required|email',
];
}
}
@@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserUpdateInfoRequest extends FormRequest
{
public function rules(): array
{
return [
'username' => 'required|min:4|max:20',
'nickname' => 'required|min:4|max:20',
'gender' => 'required',
'email' => 'required|email',
'avatar_id' => 'required|integer',
'mobile' => 'required|regex:/^1[34578]\d{9}$/',
];
}
}
+6 -6
View File
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 小程序购物车模型(门店订货车:按用户归属,同商品唯一行、加购合并数量)
* 小程序购物车模型(门店订货车:按门店归属,同商品唯一行、加购合并数量)
*/
class CartModel extends Model
{
@@ -17,23 +17,23 @@ class CartModel extends Model
protected $primaryKey = 'id';
protected $fillable = [
'user_id',
'store_id',
'product_id',
'quantity',
];
protected $casts = [
'user_id' => 'integer',
'store_id' => 'integer',
'product_id' => 'integer',
'quantity' => 'decimal:2',
];
/**
* 归属用户
* 归属门店
*/
public function user(): BelongsTo
public function store(): BelongsTo
{
return $this->belongsTo(UserModel::class, 'user_id', 'id');
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
}
/**
+13 -4
View File
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
use Modules\SystemTool\Models\SysFileModel;
/**
* 客户等级模型(同一商品按客户等级定价
* 客户等级模型(同一商品按等级上浮比例定价:售价 = 成本价 × (100 + percent) / 100
*/
class CustomerLevelModel extends Model
{
@@ -25,12 +25,14 @@ class CustomerLevelModel extends Model
protected $fillable = [
'name',
'percent',
'sort',
'status',
'icon_id'
];
protected $casts = [
'percent' => 'decimal:2',
'sort' => 'integer',
'status' => 'integer',
'created_at' => 'datetime:Y-m-d H:i:s',
@@ -65,10 +67,17 @@ class CustomerLevelModel extends Model
}
/**
* 等级下的商品价格
* 等级上浮比例计算售价(统一换算入口,金额走 bcmath 保证两位小数精度)
*
* 售价 = 成本价 × (100 + percent) / 100(四舍五入保留两位)。
*
* @param string|int|float $costPrice 商品成本价(decimal cast 字符串)
* @param string|int|float $percent 价格上浮比例(30 = 上浮 30%
* @return string 两位小数字符串,如 '13.05'
*/
public function prices(): HasMany
public static function calcLevelPrice(string|int|float $costPrice, string|int|float $percent): string
{
return $this->hasMany(ProductPriceModel::class, 'level_id', 'id');
$multiplier = bcadd('100', (string) $percent, 4);
return bcdiv(bcmul((string) $costPrice, $multiplier, 4), '100', 2);
}
}
+8 -8
View File
@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 通知模型(小程序端消息:订单 / 价格变更 / 系统;user_id=0 为全员广播)
* 通知模型(小程序端消息:订单 / 价格变更 / 系统;store_id=0 为全员广播)
*/
class NoticeModel extends Model
{
@@ -22,14 +22,14 @@ class NoticeModel extends Model
/** 已读 */
public const READ = 1;
/** 全员广播时的 user_id 约定值 */
public const BROADCAST_USER_ID = 0;
/** 全员广播时的 store_id 约定值 */
public const BROADCAST_STORE_ID = 0;
protected $table = 'notice';
protected $primaryKey = 'id';
protected $fillable = [
'user_id',
'store_id',
'type',
'title',
'content',
@@ -41,15 +41,15 @@ class NoticeModel extends Model
protected $casts = [
'data' => 'array',
'is_read' => 'integer',
'user_id' => 'integer',
'store_id' => 'integer',
'read_at' => 'datetime',
];
/**
* 接收用户(user_id=0 表示全员广播,无对应用户
* 接收门店(store_id=0 表示全员广播,无对应门店
*/
public function user(): BelongsTo
public function store(): BelongsTo
{
return $this->belongsTo(UserModel::class, 'user_id', 'id');
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
}
}
-10
View File
@@ -51,7 +51,6 @@ class PaymentModel extends Model
protected $fillable = [
'payment_no',
'store_id',
'user_id',
'amount',
'pay_method',
'voucher_ids',
@@ -64,7 +63,6 @@ class PaymentModel extends Model
protected $casts = [
'store_id' => 'integer',
'user_id' => 'integer',
'amount' => 'decimal:2',
'pay_method' => 'integer',
'status' => 'integer',
@@ -124,14 +122,6 @@ class PaymentModel extends Model
return $this->hasMany(BillModel::class, 'payment_id', 'id');
}
/**
* 提交人(小程序用户)
*/
public function user(): BelongsTo
{
return $this->belongsTo(UserModel::class, 'user_id', 'id');
}
/**
* 审核人(后台系统用户)
*/
+1 -10
View File
@@ -6,12 +6,11 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Modules\SystemTool\Models\SysFileModel;
/**
* 商品档案模型(品名/规格包规/供应商/等级/多等级价格体系
* 商品档案模型(品名/规格包规/供应商/成本价;售价按客户等级上浮比例换算
*/
class ProductModel extends Model
{
@@ -96,12 +95,4 @@ class ProductModel extends Model
{
return $this->belongsTo(SupplierModel::class, 'supplier_id', 'id');
}
/**
* 多等级价格(同一商品按客户等级定价)
*/
public function prices(): HasMany
{
return $this->hasMany(ProductPriceModel::class, 'product_id', 'id');
}
}
-108
View File
@@ -1,108 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 商品价格模型(同一商品按客户等级定价,联合键 product_id + level_id
*
* 计价类型:固定价(price 即实际单价)或成本百分比(实际单价 = 成本价 × (100 + percent) / 100
*/
class ProductPriceModel extends Model
{
use HasFactory;
/** 计价类型:固定价 */
public const int PRICE_TYPE_FIXED = 0;
/** 计价类型:成本百分比(按成本价上浮 percent 百分点) */
public const int PRICE_TYPE_PERCENT = 1;
protected $table = 'product_price';
protected $primaryKey = 'id';
protected $fillable = [
'product_id',
'level_id',
'price',
'price_type',
'percent',
];
protected $casts = [
'product_id' => 'integer',
'level_id' => 'integer',
'price' => 'decimal:2',
'price_type' => 'integer',
'percent' => 'decimal:2',
];
/** 序列化时附带实际销售价(后台列表/小程序列表直接展示) */
protected $appends = ['actual_price'];
/**
* 所属商品
*/
public function product(): BelongsTo
{
return $this->belongsTo(ProductModel::class, 'product_id', 'id');
}
/**
* 所属客户等级
*/
public function level(): BelongsTo
{
return $this->belongsTo(CustomerLevelModel::class, 'level_id', 'id');
}
/**
* 按商品 + 等级筛选价格
*/
public function scopeForProductLevel($query, int $productId, int $levelId)
{
return $query->where('product_id', $productId)->where('level_id', $levelId);
}
/**
* 计算实际销售价(统一换算入口,金额走 bcmath 保证两位小数精度)
*
* 固定价返回 price 原值;成本百分比返回 cost × (100 + percent) / 100(四舍五入保留两位)。
*
* @param string|int|float $price 固定价(decimal cast 后为 '5.50' 形式字符串)
* @param string|int|float $percent 成本上浮百分点(30 = 上浮 30%
* @param string|int|float $costPrice 商品成本价(decimal cast 字符串)
* @return string 两位小数字符串,如 '13.05'
*/
public static function calcActualPrice(
int $priceType,
string|int|float $price,
string|int|float $percent,
string|int|float $costPrice,
): string {
if ($priceType === self::PRICE_TYPE_PERCENT) {
$multiplier = bcadd('100', (string) $percent, 4);
return bcdiv(bcmul((string) $costPrice, $multiplier, 4), '100', 2);
}
// 固定价:归一化为两位小数字符串
return bcadd((string) $price, '0', 2);
}
/**
* 实际销售价访问器(供 toArray 输出 actual_price
*
* 依赖 product 关系取成本价;prices 经商品 eager load 加载时逆向关系自动填充,无 N+1
* 注意:单独序列化本模型且未加载 product 关系时会触发一次查询,成本价缺失按 0 兜底。
*/
protected function getActualPriceAttribute(): string
{
return self::calcActualPrice(
(int) $this->price_type,
(string) $this->price,
(string) $this->percent,
(string) ($this->product?->cost_price ?? 0),
);
}
}
+33 -12
View File
@@ -3,17 +3,19 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* 门店模型(小程序下单主体)
* 门店模型(小程序下单主体,即客户;门店即用户,账号密码登录
*/
class StoreModel extends Model
class StoreModel extends Authenticatable
{
use SoftDeletes, HasFactory;
use HasApiTokens, SoftDeletes, HasFactory, Notifiable;
/** 状态:停用 */
public const STATUS_DISABLED = 0;
@@ -26,6 +28,10 @@ class StoreModel extends Model
protected $fillable = [
'name',
'code',
'username',
'password',
'avatar',
'last_login_at',
'level_id',
'contact',
'phone',
@@ -35,11 +41,18 @@ class StoreModel extends Model
'remark',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'level_id' => 'integer',
'payment_cycle_days' => 'integer',
'total_purchase_amount' => 'decimal:2',
'status' => 'integer',
'last_login_at' => 'datetime:Y-m-d H:i:s',
'created_at' => 'datetime:Y-m-d H:i:s',
];
/**
@@ -58,14 +71,6 @@ class StoreModel extends Model
return $this->hasMany(StoreOrderModel::class, 'store_id', 'id');
}
/**
* 绑定本门店的小程序用户
*/
public function users(): HasMany
{
return $this->hasMany(UserModel::class, 'store_id', 'id');
}
/**
* 门店账单(采购单完成后按门店生成)
*/
@@ -73,4 +78,20 @@ class StoreModel extends Model
{
return $this->hasMany(BillModel::class, 'store_id', 'id');
}
/**
* 门店购物车
*/
public function carts(): HasMany
{
return $this->hasMany(CartModel::class, 'store_id', 'id');
}
/**
* 门店通知
*/
public function notices(): HasMany
{
return $this->hasMany(NoticeModel::class, 'store_id', 'id');
}
}
-8
View File
@@ -43,12 +43,4 @@ class SupplierModel extends Model
{
return $this->hasMany(ProductModel::class, 'supplier_id', 'id');
}
/**
* 绑定本供应商的小程序用户
*/
public function users(): HasMany
{
return $this->hasMany(UserModel::class, 'supplier_id', 'id');
}
}
-68
View File
@@ -1,68 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* APP 用户模型(小程序端:门店 / 供应商用户)
*/
class UserModel extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/** 状态:停用 */
public const int STATUS_DISABLED = 0;
/** 状态:正常 */
public const int STATUS_NORMAL = 1;
protected $table = 'user';
protected $primaryKey = 'id';
protected $hidden = [
'password',
'remember_token',
];
protected $fillable = [
'username',
'email',
'password',
'nickname',
'openid',
'unionid',
'phone',
'avatar',
'store_id',
'status',
'last_login_at',
];
protected $casts = [
'email_verified_at' => 'datetime:Y-m-d H:i:s',
'last_login_at' => 'datetime:Y-m-d H:i:s',
'created_at' => 'datetime:Y-m-d H:i:s',
'store_id' => 'integer',
'status' => 'integer',
];
/**
* 关联门店
*/
public function store(): BelongsTo
{
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
}
/**
* 用户通知
*/
public function notices(): HasMany
{
return $this->hasMany(NoticeModel::class, 'user_id', 'id');
}
}
-4
View File
@@ -2,7 +2,6 @@
namespace App\Providers;
use App\Services\WechatService;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler;
use Illuminate\Support\ServiceProvider;
@@ -18,9 +17,6 @@ class AppServiceProvider extends ServiceProvider
{
$this->app->bind(ExceptionsHandler::class, \App\Exceptions\ExceptionsHandler::class);
// 单例:测试通过 setHttpClient() 注入 Mock 后,控制器解析到同一实例
$this->app->singleton(WechatService::class);
}
/**
+187
View File
@@ -0,0 +1,187 @@
<?php
namespace App\Services;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
/**
* 采购单明细共享服务:订单/采购单汇总重算 + 门店/供应商维度的商品行聚合
*
* 汇总口径(勿偏离):
* - 订单:total_quantity=Σ数量、total_weight=Σ称重、total_amount=Σ金额
* - 采购单:total_quantity=Σ数量、estimate_amount=Σ(数量×成本价)(预估成本)、total_weight=Σ称重
*/
class PurchaseItemService
{
/**
* 重算门店订单汇总(明细增删改后调用)
*/
public function recalcOrder(int $orderId): void
{
$order = StoreOrderModel::query()->find($orderId);
if ($order === null) {
return;
}
$order->total_quantity = (int) $order->items()->sum('quantity');
$order->total_weight = $order->items()->sum('weight');
$order->total_amount = $order->items()->sum('amount');
$order->save();
}
/**
* 重算采购单汇总(明细增删改后调用)
*/
public function recalcPurchase(int $purchaseId): void
{
$purchase = PurchaseOrderModel::query()->find($purchaseId);
if ($purchase === null) {
return;
}
$purchase->total_quantity = (int) StoreOrderItemModel::query()
->where('purchase_id', $purchaseId)->sum('quantity');
$purchase->estimate_amount = StoreOrderItemModel::query()
->where('purchase_id', $purchaseId)->sum(DB::raw('quantity * cost_price'));
$purchase->total_weight = StoreOrderItemModel::query()
->where('purchase_id', $purchaseId)->sum('weight');
$purchase->save();
}
/**
* 采购单内指定门店的采购汇总行(按商品聚合,单价=加权平均 Σ金额÷Σ数量,
* 排序:分类 sort 商品 sort,与明细矩阵同序)
*
* @return array<int, array<string, mixed>>
*/
public function storeSummaryRows(int $purchaseId, int $storeId): array
{
$items = $this->purchaseItems($purchaseId)
->where('store_id', $storeId)
->sortBy('id');
$rows = [];
foreach ($items->groupBy('product_id') as $productId => $group) {
$first = $group->first();
$quantity = 0;
$amount = '0';
$weight = '0';
foreach ($group as $item) {
$weight = bcadd($weight, (string) $item->weight, 3);
$quantity += (int) $item->quantity;
$amount = bcadd($amount, (string) $item->amount, 2);
}
$rows[] = [
'product_id' => (int) $productId,
'product_name' => $first->product_name,
'product_spec' => $first->product_spec,
'unit' => $first->unit,
// 加权平均单价(保证 单价×数量=预计金额)
'price' => $quantity > 0
? bcdiv($amount, (string) $quantity, 2)
: (string) $first->price,
'quantity' => $quantity,
'amount' => $amount,
'weight' => $weight,
'category_sort' => (int) data_get($first, 'category_sort', 9999),
'product_sort' => (int) data_get($first, 'product_sort', 9999),
];
}
return $this->sortRows($rows);
}
/**
* 采购单内指定供应商的商品聚合行(成本口径:金额=Σ数量×成本价,
* 排序:分类 sort 商品 sort
*
* @return array<int, array<string, mixed>>
*/
public function supplierRows(int $purchaseId, int $supplierId): array
{
$items = $this->purchaseItems($purchaseId)
->where('supplier_id', $supplierId)
->sortBy('id');
$rows = [];
foreach ($items->groupBy('product_id') as $productId => $group) {
$first = $group->first();
$quantity = 0;
$weight = '0';
$amount = '0';
foreach ($group as $item) {
$quantity += (int) $item->quantity;
$weight = bcadd($weight, (string) $item->weight, 3);
$amount = bcadd($amount, bcmul((string) $item->quantity, (string) $item->cost_price, 2), 2);
}
$rows[] = [
'product_id' => (int) $productId,
'product_name' => $first->product_name,
'product_spec' => $first->product_spec,
'unit' => $first->unit,
'cost_price' => (string) $first->cost_price,
'quantity' => $quantity,
'weight' => $weight,
'amount' => $amount,
'category_sort' => (int) data_get($first, 'category_sort', 9999),
'product_sort' => (int) data_get($first, 'product_sort', 9999),
];
}
return $this->sortRows($rows);
}
/**
* 采购单订货明细(关联订单过滤软删、成本价可见、附分类/商品排序键)
*
* @return Collection<int, StoreOrderItemModel>
*/
private function purchaseItems(int $purchaseId): Collection
{
$items = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $purchaseId)
->whereNull('store_order.deleted_at')
->select('store_order_item.*')
->get()
->makeVisible('cost_price');
// 排序键:商品分类 sort → 商品 sort(商品含软删除,保证历史单据可导出)
$products = ProductModel::withTrashed()
->with('category:id,sort')
->whereIn('id', $items->pluck('product_id')->unique())
->get(['id', 'category_id', 'sort'])
->keyBy('id');
foreach ($items as $item) {
$product = $products->get((int) $item->product_id);
$item->setAttribute('category_sort', (int) ($product?->category?->sort ?? 9999));
$item->setAttribute('product_sort', (int) ($product?->sort ?? 9999));
}
return $items;
}
/**
* 排序并剥离排序键
*
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function sortRows(array $rows): array
{
usort($rows, static fn (array $a, array $b): int =>
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
return array_map(static function (array $row): array {
unset($row['category_sort'], $row['product_sort']);
return $row;
}, $rows);
}
}
-99
View File
@@ -1,99 +0,0 @@
<?php
namespace App\Services;
use App\Exceptions\RepositoryException;
use EasyWeChat\Kernel\Exceptions\HttpException;
use EasyWeChat\MiniApp\Application;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* 微信小程序服务(基于 EasyWeChat 6.x
*
* 封装 code2Session / 手机号解密;配置读取 site_config('wechatMini')
* (后台「小程序设置」面板维护,存 sys_site_config 表)。
*
* 测试策略:通过 setHttpClient() 注入 Symfony MockHttpClient 拦截微信 HTTP 调用。
*/
class WechatService
{
private ?Application $app = null;
private ?HttpClientInterface $httpClient = null;
/**
* 注入自定义 HttpClient(测试注入 MockHttpClient;注入后强制重建 Application
*/
public function setHttpClient(HttpClientInterface $httpClient): void
{
$this->httpClient = $httpClient;
$this->app = null;
}
/**
* code2Session:小程序 wx.login code 换取 openid / session_key
*
* @param string $code wx.login 返回的临时登录凭证
* @return array{openid: string, session_key: string, unionid?: string}
*/
public function code2Session(string $code): array
{
try {
/** @var array{openid: string, session_key: string, unionid?: string} $session */
$session = $this->app()->getUtils()->codeToSession($code);
} catch (HttpException|TransportExceptionInterface $e) {
throw new RepositoryException('微信登录失败:' . $e->getMessage());
}
return $session;
}
/**
* 获取手机号:wx.getPhoneNumber phoneCode 换取手机号
*
* @param string $phoneCode 手机号授权事件返回的动态令牌
* @return string 用户手机号
*/
public function getPhone(string $phoneCode): string
{
try {
$result = $this->app()->getUtils()->getPhoneNumber($phoneCode);
} catch (HttpException|TransportExceptionInterface $e) {
throw new RepositoryException('获取手机号失败:' . $e->getMessage());
}
$phone = (string) ($result['phone_info']['phoneNumber']
?? $result['phone_info']['purePhoneNumber']
?? '');
if ($phone === '') {
throw new RepositoryException('获取手机号失败:微信未返回有效手机号');
}
return $phone;
}
/**
* EasyWeChat 小程序应用实例(懒构建单例)
*/
protected function app(): Application
{
if ($this->app === null) {
$config = (array) site_config('wechatMini', []);
if (empty($config['appid']) || empty($config['secret'])) {
throw new RepositoryException('微信小程序尚未配置(WECHAT_MINI_APPID / WECHAT_MINI_SECRET');
}
$this->app = new Application([
'app_id' => (string) $config['appid'],
'secret' => (string) $config['secret'],
]);
if ($this->httpClient !== null) {
$this->app->setHttpClient($this->httpClient);
}
}
return $this->app;
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ return [
],
'users' => [
'driver' => 'eloquent',
'model' => \App\Models\UserModel::class
'model' => \App\Models\StoreModel::class
],
],
@@ -1,31 +0,0 @@
<?php
namespace Database\Factories;
use App\Models\ProductPriceModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 商品等级价格工厂(product_id / level_id 需调用方指定;无需 Faker
*
* @extends Factory<ProductPriceModel>
*/
class ProductPriceModelFactory extends Factory
{
protected $model = ProductPriceModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'product_id' => 0,
'level_id' => 0,
'price' => number_format(random_int(100, 10000) / 100 + $seq * 0.01, 2, '.', ''),
'price_type' => ProductPriceModel::PRICE_TYPE_FIXED,
'percent' => 0,
];
}
}
+12
View File
@@ -23,6 +23,10 @@ class StoreModelFactory extends Factory
return [
'name' => '测试门店' . $seq,
'code' => 'S' . str_pad((string) $seq, 6, '0', STR_PAD_LEFT),
'username' => 'store' . $seq,
'password' => password_hash('123456', PASSWORD_DEFAULT),
'avatar' => '',
'last_login_at' => null,
'level_id' => 0,
'contact' => '联系人' . $seq,
'phone' => '138' . str_pad((string) $seq, 8, '0', STR_PAD_LEFT),
@@ -41,6 +45,14 @@ class StoreModelFactory extends Factory
return $this->state(fn () => ['status' => StoreModel::STATUS_DISABLED]);
}
/**
* 指定登录密码(明文,入库自动哈希)
*/
public function withPassword(string $password): static
{
return $this->state(fn () => ['password' => password_hash($password, PASSWORD_DEFAULT)]);
}
/**
* 指定回款周期(天)
*/
-55
View File
@@ -1,55 +0,0 @@
<?php
namespace Database\Factories;
use App\Models\UserModel;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 小程序用户工厂(微信登录自动生成,供测试使用;无需 Faker
*
* @extends Factory<UserModel>
*/
class UserModelFactory extends Factory
{
protected $model = UserModel::class;
private static int $sequence = 0;
public function definition(): array
{
$seq = ++self::$sequence;
return [
'username' => null,
'password' => null,
'nickname' => '微信用户' . $seq,
'email' => '',
'openid' => 'openid_' . str_pad((string) $seq, 16, '0', STR_PAD_LEFT),
'unionid' => '',
'phone' => '',
'avatar' => '',
'store_id' => 0,
'status' => UserModel::STATUS_NORMAL,
'last_login_at' => null,
];
}
/**
* 已绑定门店的门店用户
*/
public function forStore(int $storeId): static
{
return $this->state(fn () => [
'store_id' => $storeId,
]);
}
/**
* 停用账号
*/
public function disabled(): static
{
return $this->state(fn () => ['status' => UserModel::STATUS_DISABLED]);
}
}
@@ -12,36 +12,13 @@ return new class extends Migration
*/
public function up(): void
{
if (! Schema::hasTable('user')) {
Schema::create('user', function (Blueprint $table) {
$table->increments('id')->comment('用户ID');
$table->string('username', 20)->nullable()->unique()->comment('用户名');
$table->string('password', 100)->nullable()->comment('密码');
$table->string('nickname', 20)->default('')->comment('昵称');
$table->string('email', 50)->default('')->comment('邮箱');
$table->timestamp('email_verified_at')->nullable();
// 小程序用户扩展字段
$table->string('openid', 64)->nullable()->unique()->comment('微信OpenID');
$table->string('unionid', 64)->default('')->comment('微信UnionID');
$table->string('phone', 20)->default('')->comment('手机号');
$table->string('avatar', 255)->default('')->comment('头像');
$table->integer('store_id')->default(0)->comment('关联门店ID');
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
$table->timestamp('last_login_at')->nullable()->comment('最后登录时间');
$table->rememberToken();
$table->timestamps();
$table->index('store_id', 'user_type_index');
$table->index('openid', 'user_openid_index');
$table->comment('APP用户表(含小程序用户)');
});
}
// 客户等级表(价格体系按等级定价)
if (! Schema::hasTable('customer_level')) {
Schema::create('customer_level', function (Blueprint $table) {
$table->increments('id')->comment('等级ID');
$table->integer('icon_id')->nullable()->comment('等级图标ID');
$table->string('name', 50)->comment('等级名称(如:一级客户、二级客户)');
$table->decimal('percent', 5, 2)->default(0)->comment('价格上浮比例(%,如 30 = 成本价上浮30%');
$table->integer('sort')->default(0)->comment('排序');
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
$table->timestamps();
@@ -55,6 +32,11 @@ return new class extends Migration
$table->increments('id')->comment('门店ID');
$table->string('name', 100)->comment('门店名称');
$table->string('code', 50)->unique()->comment('门店编码');
// 登录账号字段(小程序端 账号 + 密码 登录)
$table->string('username', 20)->unique()->comment('登录账号');
$table->string('password', 100)->comment('登录密码');
$table->string('avatar', 255)->default('')->comment('头像');
$table->timestamp('last_login_at')->nullable()->comment('最后登录时间');
$table->integer('level_id')->default(0)->comment('客户等级ID(决定商品价格)');
$table->string('contact', 50)->default('')->comment('联系人');
$table->string('phone', 20)->default('')->comment('联系电话');
@@ -63,10 +45,11 @@ return new class extends Migration
$table->decimal('total_purchase_amount', 12, 2)->default(0)->comment('总采购金额(只统计商品金额,账单支付后累加)');
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
$table->string('remark', 255)->nullable()->default('')->comment('备注');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->index(['level_id', 'status'], 'store_level_status_index');
$table->comment('门店表');
$table->comment('门店表(门店即用户,账号密码登录)');
});
}
@@ -91,7 +74,7 @@ return new class extends Migration
if (! Schema::hasTable('notice')) {
Schema::create('notice', function (Blueprint $table) {
$table->increments('id')->comment('通知ID');
$table->integer('user_id')->default(0)->comment('接收用户IDuser表,0为全员广播)');
$table->integer('store_id')->default(0)->comment('接收门店IDstore表,0为全员广播)');
$table->string('type', 20)->default('system')->comment('通知类型(order订单 price价格 system系统)');
$table->string('title', 100)->comment('标题');
$table->string('content', 500)->default('')->comment('内容');
@@ -99,7 +82,7 @@ return new class extends Migration
$table->integer('is_read')->default(0)->comment('是否已读(1已读 0未读)');
$table->timestamp('read_at')->nullable()->comment('阅读时间');
$table->timestamps();
$table->index(['user_id', 'is_read'], 'notice_user_read_index');
$table->index(['store_id', 'is_read'], 'notice_store_read_index');
$table->comment('消息通知表');
});
}
@@ -110,7 +93,6 @@ return new class extends Migration
*/
public function down(): void
{
Schema::dropIfExists('user');
Schema::dropIfExists('customer_level');
Schema::dropIfExists('store');
Schema::dropIfExists('supplier');
@@ -8,7 +8,7 @@ return new class extends Migration
{
/**
* Run the migrations.
* 商品中心(A1-A3):商品分类、商品档案、客户等级价格体系
* 商品中心(A1-A3):商品分类、商品档案等级价格 = 成本价 × customer_level.percent 上浮)
*/
public function up(): void
{
@@ -50,21 +50,6 @@ return new class extends Migration
$table->comment('商品表');
});
}
// 商品等级价格表
if (! Schema::hasTable('product_price')) {
Schema::create('product_price', function (Blueprint $table) {
$table->increments('id')->comment('价格ID');
$table->integer('product_id')->comment('商品ID');
$table->integer('level_id')->comment('客户等级ID');
$table->decimal('price', 10, 2)->default(0)->comment('该等级下的商品单价(固定价=实际单价;百分比=等价固定价)');
$table->unsignedTinyInteger('price_type')->default(0)->comment('计价类型(0固定价 1成本百分比)');
$table->decimal('percent', 5, 2)->default(0)->comment('成本上浮百分点(如 30 = 上浮30%,仅 price_type=1 生效)');
$table->timestamps();
$table->unique(['product_id', 'level_id'], 'product_price_product_level_unique');
$table->comment('商品等级价格表');
});
}
}
/**
@@ -74,6 +59,5 @@ return new class extends Migration
{
Schema::dropIfExists('product_category');
Schema::dropIfExists('product');
Schema::dropIfExists('product_price');
}
};
@@ -8,19 +8,19 @@ return new class extends Migration
{
/**
* Run the migrations.
* 小程序购物车(门店订货车):按用户归属,同商品唯一行、加购合并累加
* 小程序购物车(门店订货车):按门店归属,同商品唯一行、加购合并累加
*/
public function up(): void
{
if (! Schema::hasTable('cart')) {
Schema::create('cart', function (Blueprint $table) {
$table->increments('id')->comment('购物车项ID');
$table->integer('user_id')->comment('用户ID(购物车归属者)');
$table->integer('store_id')->comment('门店ID(购物车归属者)');
$table->integer('product_id')->comment('商品ID');
$table->decimal('quantity', 10, 2)->default(0)->comment('订货量');
$table->timestamps();
$table->unique(['user_id', 'product_id'], 'cart_user_product_unique');
$table->index(['user_id', 'created_at'], 'cart_user_created_index');
$table->unique(['store_id', 'product_id'], 'cart_store_product_unique');
$table->index(['store_id', 'created_at'], 'cart_store_created_index');
$table->comment('小程序购物车表(门店订货车)');
});
}
@@ -16,8 +16,7 @@ return new class extends Migration
Schema::create('payment', function (Blueprint $table) {
$table->increments('id')->comment('支付记录ID');
$table->string('payment_no', 32)->unique()->comment('支付单号');
$table->integer('store_id')->comment('门店ID');
$table->integer('user_id')->default(0)->comment('提交人(小程序用户ID');
$table->integer('store_id')->comment('门店ID(提交门店即支付人)');
$table->decimal('amount', 10, 2)->default(0)->comment('支付金额(= 关联账单总金额合计,提交时快照)');
$table->integer('pay_method')->comment('支付方式(1微信 2支付宝 3对公汇款)');
$table->string('voucher_ids', 255)->default('')->comment('汇款凭证图片ID(逗号分隔)');
-11
View File
@@ -135,17 +135,6 @@ class PermissionSeeder extends Seeder
['type' => 'rule', 'key' => 'customer.level.delete', 'name' => '删除'],
],
],
[
'type' => 'route',
'key' => 'customer.miniUser',
'name' => '小程序用户',
'path' => '/customer/mini-user',
'children' => [
['type' => 'rule', 'key' => 'customer.miniUser.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'customer.miniUser.update', 'name' => '启用停用'],
['type' => 'rule', 'key' => 'customer.miniUser.bind', 'name' => '绑定主体'],
],
],
[
'type' => 'route',
'key' => 'customer.notice',
+1 -4
View File
@@ -16,8 +16,7 @@ class SysDataSeeder extends Seeder
// 系统设置初始数据
DB::table('sys_site_config_group')->insert([
['id' => 1, 'title' => '网站设置', 'key' => 'web', 'remark' => '网站基础设置', 'created_at' => $date, 'updated_at' => $date],
['id' => 2, 'title' => '小程序设置', 'key' => 'wechatMini', 'remark' => '小程序设置', 'created_at' => $date, 'updated_at' => $date],
['id' => 3, 'title' => '业务配置', 'key' => 'services', 'remark' => '小程序设置', 'created_at' => $date, 'updated_at' => $date],
['id' => 3, 'title' => '业务配置', 'key' => 'services', 'remark' => '业务附加配置', 'created_at' => $date, 'updated_at' => $date],
['id' => 4, 'title' => '支付配置', 'key' => 'pay', 'remark' => '网站的支付配置', 'created_at' => $date, 'updated_at' => $date],
]);
DB::table('sys_site_config_items')->insert([
@@ -25,8 +24,6 @@ class SysDataSeeder extends Seeder
['id' => 2, 'group_id' => 1, 'key' => 'logo', 'title' => '网站LOGO', 'describe' => '网站的LOGO,用于标识网站', 'values' => 'https://file.xinadmin.cn/file/favicons.ico', 'type' => 'Input','options' => "", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date,],
['id' => 3, 'group_id' => 1, 'key' => 'subtitle', 'title' => '网站副标题', 'describe' => '网站副标题,展示在登录页面标题的下面', 'values' => 'Xin Admin 快速开发框架', 'type' => 'Input','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date,],
['id' => 4, 'group_id' => 1, 'key' => 'describe', 'title' => '网站描述', 'describe' => '网站的基本描述', 'values' => '没有描述', 'type' => 'TextArea','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
['id' => 5, 'group_id' => 2, 'key' => 'appid', 'title' => 'APPID', 'describe' => '小程序的APPID', 'values' => '没有描述', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
['id' => 6, 'group_id' => 2, 'key' => 'secret', 'title' => 'SecretKey', 'describe' => '小程序的SecretKey', 'values' => '没有描述', 'type' => 'Input','options' => "", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
['id' => 7, 'group_id' => 3, 'key' => 'box_amount', 'title' => '周转筐金额', 'describe' => '周转筐的金额,用于附加业务金额的计算', 'values' => '', 'type' => 'InputNumber','props' => "min=0\nmax=10000\nstep=0.01", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
['id' => 8, 'group_id' => 3, 'key' => 'tray_amount', 'title' => '周转托盘金额', 'describe' => '周转托盘的金额,用于附加金额的计算', 'values' => '', 'type' => 'InputNumber','props' => "min=0\nmax=10000\nstep=0.01", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
['id' => 9, 'group_id' => 4, 'key' => 'wechat_qrcode', 'title' => '微信收款码', 'describe' => '微信收款码图片地址(在文件管理中上传收款码后复制图片链接,或直接填写文件ID),小程序付款页展示', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
-1
View File
@@ -1 +0,0 @@
import"./rolldown-runtime-BgaNhQyE.js";import{Dr as e,t}from"./jsx-runtime-CRBytmvs.js";import{t as n}from"./button-BILozH6U.js";import{t as r}from"./card-DU2T2-YW.js";import{t as i}from"./result-D_u5wtF_.js";e();var a=t(),o=()=>(0,a.jsx)(r,{variant:`borderless`,children:(0,a.jsx)(i,{status:`403`,title:`403`,subTitle:`Sorry, you are not authorized to access this page.`,extra:(0,a.jsx)(n,{type:`primary`,children:`Back Home`})})});export{o as default};
-1
View File
@@ -1 +0,0 @@
import"./rolldown-runtime-BgaNhQyE.js";import{Dr as e,t}from"./jsx-runtime-CRBytmvs.js";import{t as n}from"./button-BILozH6U.js";import{t as r}from"./card-DU2T2-YW.js";import{t as i}from"./result-D_u5wtF_.js";e();var a=t(),o=()=>(0,a.jsx)(r,{variant:`borderless`,children:(0,a.jsx)(i,{status:`404`,title:`404`,subTitle:`Sorry, the page you visited does not exist.`,extra:(0,a.jsx)(n,{type:`primary`,children:`Back Home`})})});export{o as default};
-1
View File
@@ -1 +0,0 @@
import"./rolldown-runtime-BgaNhQyE.js";import{Dr as e,t}from"./jsx-runtime-CRBytmvs.js";import{t as n}from"./button-BILozH6U.js";import{t as r}from"./card-DU2T2-YW.js";import{t as i}from"./result-D_u5wtF_.js";e();var a=t(),o=()=>(0,a.jsx)(r,{variant:`borderless`,children:(0,a.jsx)(i,{status:`500`,title:`500`,subTitle:`Sorry, something went wrong.`,extra:(0,a.jsx)(n,{type:`primary`,children:`Back Home`})})});export{o as default};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0048.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z`}}]},name:`arrow-down`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M868 545.5L536.1 163a31.96 31.96 0 00-48.3 0L156 545.5a7.97 7.97 0 006 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z`}}]},name:`arrow-up`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
-1
View File
@@ -1 +0,0 @@
import{i as e}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as t,t as n}from"./jsx-runtime-CRBytmvs.js";import{t as r}from"./user-LBumqaa7.js";var i=e(t(),1),a=n(),o=({auth:e,children:t})=>{let n=r(e=>e.access);return(0,i.useMemo)(()=>!e||n.includes(e),[n,e])?(0,a.jsx)(a.Fragment,{children:t}):null};export{o as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`0 0 1024 1024`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z`}}]},name:`caret-down`,theme:`filled`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z`}}]},name:`check-circle`,theme:`filled`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z`}},{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}}]},name:`check-circle`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M682 455V311l-76 76v68c-.1 50.7-42 92.1-94 92a95.8 95.8 0 01-52-15l-54 55c29.1 22.4 65.9 36 106 36 93.8 0 170-75.1 170-168z`}},{tag:`path`,attrs:{d:`M833 446h-60c-4.4 0-8 3.6-8 8 0 140.3-113.7 254-254 254-63 0-120.7-23-165-61l-54 54a334.01 334.01 0 00179 81v102H326c-13.9 0-24.9 14.3-25 32v36c.1 4.4 2.9 8 6 8h408c3.2 0 6-3.6 6-8v-36c0-17.7-11-32-25-32H547V782c165.3-17.9 294-157.9 294-328 0-4.4-3.6-8-8-8zm13.1-377.7l-43.5-41.9a8 8 0 00-11.2.1l-129 129C634.3 101.2 577 64 511 64c-93.9 0-170 75.3-170 168v224c0 6.7.4 13.3 1.2 19.8l-68 68A252.33 252.33 0 01258 454c-.2-4.4-3.8-8-8-8h-60c-4.4 0-8 3.6-8 8 0 53 12.5 103 34.6 147.4l-137 137a8.03 8.03 0 000 11.3l42.7 42.7c3.1 3.1 8.2 3.1 11.3 0L846.2 79.8l.1-.1c3.1-3.2 3-8.3-.2-11.4zM417 401V232c0-50.6 41.9-92 94-92 46 0 84.1 32.3 92.3 74.7L417 401z`}}]},name:`audio-muted`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default}))),l=e(t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M842 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 140.3-113.7 254-254 254S258 594.3 258 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 168.7 126.6 307.9 290 327.6V884H326.7c-13.7 0-24.7 14.3-24.7 32v36c0 4.4 2.8 8 6.2 8h407.6c3.4 0 6.2-3.6 6.2-8v-36c0-17.7-11-32-24.7-32H548V782.1c165.3-18 294-158 294-328.1zM512 624c93.9 0 170-75.2 170-168V232c0-92.8-76.1-168-170-168s-170 75.2-170 168v224c0 92.8 76.1 168 170 168zm-94-392c0-50.6 41.9-92 94-92s94 41.4 94 92v224c0 50.6-41.9 92-94 92s-94-41.4-94-92V232z`}}]},name:`audio`,theme:`outlined`}}))());function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},u.apply(this,arguments)}var d=a.forwardRef((e,t)=>a.createElement(r,u({},e,{ref:t,icon:l.default}))),f=e(t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M899.1 869.6l-53-305.6H864c14.4 0 26-11.6 26-26V346c0-14.4-11.6-26-26-26H618V138c0-14.4-11.6-26-26-26H432c-14.4 0-26 11.6-26 26v182H160c-14.4 0-26 11.6-26 26v192c0 14.4 11.6 26 26 26h17.9l-53 305.6a25.95 25.95 0 0025.6 30.4h723c1.5 0 3-.1 4.4-.4a25.88 25.88 0 0021.2-30zM204 390h272V182h72v208h272v104H204V390zm468 440V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H416V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H202.8l45.1-260H776l45.1 260H672z`}}]},name:`clear`,theme:`outlined`}}))());function p(){return p=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},p.apply(this,arguments)}var m=a.forwardRef((e,t)=>a.createElement(r,p({},e,{ref:t,icon:f.default})));export{d as n,c as r,m as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}},{tag:`path`,attrs:{d:`M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z`}}]},name:`clock-circle`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{"fill-rule":`evenodd`,viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z`}}]},name:`close-circle`,theme:`filled`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{"fill-rule":`evenodd`,viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z`}}]},name:`close-circle`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z`}}]},name:`delete`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z`}}]},name:`down`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z`}}]},name:`download`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M854.6 289.1a362.49 362.49 0 00-79.9-115.7 370.83 370.83 0 00-118.2-77.8C610.7 76.6 562.1 67 512 67c-50.1 0-98.7 9.6-144.5 28.5-44.3 18.3-84 44.5-118.2 77.8A363.6 363.6 0 00169.4 289c-19.5 45-29.4 92.8-29.4 142 0 70.6 16.9 140.9 50.1 208.7 26.7 54.5 64 107.6 111 158.1 80.3 86.2 164.5 138.9 188.4 153a43.9 43.9 0 0022.4 6.1c7.8 0 15.5-2 22.4-6.1 23.9-14.1 108.1-66.8 188.4-153 47-50.4 84.3-103.6 111-158.1C867.1 572 884 501.8 884 431.1c0-49.2-9.9-97-29.4-142zM512 880.2c-65.9-41.9-300-207.8-300-449.1 0-77.9 31.1-151.1 87.6-206.3C356.3 169.5 431.7 139 512 139s155.7 30.5 212.4 85.9C780.9 280 812 353.2 812 431.1c0 241.3-234.1 407.2-300 449.1zm0-617.2c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 551c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 439c0-29.9 11.7-58 32.8-79.2C454 338.6 482.1 327 512 327c29.9 0 58 11.6 79.2 32.8C612.4 381 624 409.1 624 439c0 29.9-11.6 58-32.8 79.2z`}}]},name:`environment`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z`}}]},name:`exclamation-circle`,theme:`filled`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z`}}]},name:`eye`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M296 250c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H296zm184 144H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm-48 458H208V148h560v320c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V108c0-17.7-14.3-32-32-32H168c-17.7 0-32 14.3-32 32v784c0 17.7 14.3 32 32 32h264c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm440-88H728v-36.6c46.3-13.8 80-56.6 80-107.4 0-61.9-50.1-112-112-112s-112 50.1-112 112c0 50.7 33.7 93.6 80 107.4V764H520c-8.8 0-16 7.2-16 16v152c0 8.8 7.2 16 16 16h352c8.8 0 16-7.2 16-16V780c0-8.8-7.2-16-16-16zM646 620c0-27.6 22.4-50 50-50s50 22.4 50 50-22.4 50-50 50-50-22.4-50-50zm180 266H566v-60h260v60z`}}]},name:`audit`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default}))),l=e(t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{"fill-rule":`evenodd`,viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M880 912H144c-17.7 0-32-14.3-32-32V144c0-17.7 14.3-32 32-32h360c4.4 0 8 3.6 8 8v56c0 4.4-3.6 8-8 8H184v656h656V520c0-4.4 3.6-8 8-8h56c4.4 0 8 3.6 8 8v360c0 17.7-14.3 32-32 32zM770.87 199.13l-52.2-52.2a8.01 8.01 0 014.7-13.6l179.4-21c5.1-.6 9.5 3.7 8.9 8.9l-21 179.4c-.8 6.6-8.9 9.4-13.6 4.7l-52.4-52.4-256.2 256.2a8.03 8.03 0 01-11.3 0l-42.4-42.4a8.03 8.03 0 010-11.3l256.1-256.3z`}}]},name:`export`,theme:`outlined`}}))());function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},u.apply(this,arguments)}var d=a.forwardRef((e,t)=>a.createElement(r,u({},e,{ref:t,icon:l.default}))),f=e(t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M688 312v-48c0-4.4-3.6-8-8-8H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8zm-392 88c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H296zm376 116c-119.3 0-216 96.7-216 216s96.7 216 216 216 216-96.7 216-216-96.7-216-216-216zm107.5 323.5C750.8 868.2 712.6 884 672 884s-78.8-15.8-107.5-44.5C535.8 810.8 520 772.6 520 732s15.8-78.8 44.5-107.5C593.2 595.8 631.4 580 672 580s78.8 15.8 107.5 44.5C808.2 653.2 824 691.4 824 732s-15.8 78.8-44.5 107.5zM761 656h-44.3c-2.6 0-5 1.2-6.5 3.3l-63.5 87.8-23.1-31.9a7.92 7.92 0 00-6.5-3.3H573c-6.5 0-10.3 7.4-6.5 12.7l73.8 102.1c3.2 4.4 9.7 4.4 12.9 0l114.2-158c3.9-5.3.1-12.7-6.4-12.7zM440 852H208V148h560v344c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V108c0-17.7-14.3-32-32-32H168c-17.7 0-32 14.3-32 32v784c0 17.7 14.3 32 32 32h272c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z`}}]},name:`file-done`,theme:`outlined`}}))());function p(){return p=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},p.apply(this,arguments)}var m=a.forwardRef((e,t)=>a.createElement(r,p({},e,{ref:t,icon:f.default})));export{d as n,c as r,m as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z`}}]},name:`file`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z`}}]},name:`github`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default}))),l=e(t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M536.1 273H488c-4.4 0-8 3.6-8 8v275.3c0 2.6 1.2 5 3.3 6.5l165.3 120.7c3.6 2.6 8.6 1.9 11.2-1.7l28.6-39c2.7-3.7 1.9-8.7-1.7-11.2L544.1 528.5V281c0-4.4-3.6-8-8-8zm219.8 75.2l156.8 38.3c5 1.2 9.9-2.6 9.9-7.7l.8-161.5c0-6.7-7.7-10.5-12.9-6.3L752.9 334.1a8 8 0 003 14.1zm167.7 301.1l-56.7-19.5a8 8 0 00-10.1 4.8c-1.9 5.1-3.9 10.1-6 15.1-17.8 42.1-43.3 80-75.9 112.5a353 353 0 01-112.5 75.9 352.18 352.18 0 01-137.7 27.8c-47.8 0-94.1-9.3-137.7-27.8a353 353 0 01-112.5-75.9c-32.5-32.5-58-70.4-75.9-112.5A353.44 353.44 0 01171 512c0-47.8 9.3-94.2 27.8-137.8 17.8-42.1 43.3-80 75.9-112.5a353 353 0 01112.5-75.9C430.6 167.3 477 158 524.8 158s94.1 9.3 137.7 27.8A353 353 0 01775 261.7c10.2 10.3 19.8 21 28.6 32.3l59.8-46.8C784.7 146.6 662.2 81.9 524.6 82 285 82.1 92.6 276.7 95 516.4 97.4 751.9 288.9 942 524.8 942c185.5 0 343.5-117.6 403.7-282.3 1.5-4.2-.7-8.9-4.9-10.4z`}}]},name:`history`,theme:`outlined`}}))());function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},u.apply(this,arguments)}var d=a.forwardRef((e,t)=>a.createElement(r,u({},e,{ref:t,icon:l.default}))),f=e(t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}},{tag:`path`,attrs:{d:`M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z`}}]},name:`info-circle`,theme:`outlined`}}))());function p(){return p=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},p.apply(this,arguments)}var m=a.forwardRef((e,t)=>a.createElement(r,p({},e,{ref:t,icon:f.default})));export{d as n,c as r,m as t};
-1
View File
@@ -1 +0,0 @@
import{i as e}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as t,Q as n,St as r,Vn as i,Z as a,at as o,lr as ee,p as te,u as ne,xt as s,yt as c,zn as l}from"./jsx-runtime-CRBytmvs.js";import{f as u,m as d,p as f}from"./tooltip-SaeG1Uv7.js";import{a as p,o as m}from"./style-BmYds38x.js";import{r as re,t as ie}from"./es-DGcDZmXr.js";var h=e(t());function g(e,t){let n=(0,h.useRef)([]),r=()=>{n.current.push(setTimeout(()=>{e.current?.input&&e.current?.input.getAttribute(`type`)===`password`&&e.current?.input.hasAttribute(`value`)&&e.current?.input.removeAttribute(`value`)}))};return(0,h.useEffect)(()=>(t&&r(),()=>n.current.forEach(e=>{e&&clearTimeout(e)})),[t]),r}function ae(e){return!!(e.prefix||e.suffix||e.allowClear||e.showCount)}var _=(0,h.forwardRef)((e,t)=>{let{prefixCls:_,bordered:oe=!0,status:se,size:ce,disabled:le,onBlur:ue,onFocus:v,suffix:y,allowClear:b,addonAfter:x,addonBefore:S,className:C,style:w,styles:T,rootClassName:E,onChange:D,classNames:O,variant:k,...A}=e,{getPrefixCls:j,direction:M,allowClear:N,autoComplete:P,className:F,style:I,classNames:L,styles:R}=l(`input`),z=j(`input`,_),B=(0,h.useRef)(null),V=c(z),[H,U]=m(z,E);p(z,V);let{compactSize:de,compactItemClassnames:fe}=a(z,M),W=n(e=>ce??de??e),pe=h.useContext(o),G=le??pe,me={...e,size:W,disabled:G},he=r(I),ge=r(w),[K,q]=s([L,O],[R,he,T,ge],{props:me}),{status:_e,hasFeedback:J,feedbackIcon:ve}=(0,h.useContext)(te),Y=u(_e,se);(0,h.useRef)(ae(e)||!!J);let X=g(B,!0),Z=e=>{X(),ue?.(e)},ye=e=>{X(),v?.(e)},be=e=>{X(),D?.(e)},xe=(J||y)&&h.createElement(h.Fragment,null,y,J&&ve),Se=re({allowClear:b,contextAllowClear:N,componentName:`Input`}),[Q,$]=ne(`input`,k,oe);return h.createElement(ie,{ref:ee(t,B),prefixCls:z,autoComplete:P,...A,disabled:G,onBlur:Z,onFocus:ye,style:q.root,styles:q,suffix:xe,allowClear:Se,className:i(C,E,U,V,fe,F,K.root),onChange:be,addonBefore:S&&h.createElement(d,{form:!0,space:!0},S),addonAfter:x&&h.createElement(d,{form:!0,space:!0},x),classNames:{...K,input:i({[`${z}-sm`]:W===`small`,[`${z}-lg`]:W===`large`,[`${z}-rtl`]:M===`rtl`},K.input,H),variant:i({[`${z}-${Q}`]:$},f(z,Y)),affixWrapper:i({[`${z}-affix-wrapper-sm`]:W===`small`,[`${z}-affix-wrapper-lg`]:W===`large`,[`${z}-affix-wrapper-rtl`]:M===`rtl`},H),wrapper:i({[`${z}-group-rtl`]:M===`rtl`},H),groupWrapper:i({[`${z}-group-wrapper-sm`]:W===`small`,[`${z}-group-wrapper-lg`]:W===`large`,[`${z}-group-wrapper-rtl`]:M===`rtl`,[`${z}-group-wrapper-${Q}`]:$},f(`${z}-group-wrapper`,Y,J),H)}})});export{g as n,_ as t};
-4
View File
@@ -1,4 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{An as n,Dr as r,Ft as i,J as a}from"./jsx-runtime-CRBytmvs.js";var o=new n(`antMoveDownIn`,{"0%":{transform:`translate3d(0, 100%, 0)`,transformOrigin:`0 0`,opacity:0},"100%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1}}),s=new n(`antMoveDownOut`,{"0%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1},"100%":{transform:`translate3d(0, 100%, 0)`,transformOrigin:`0 0`,opacity:0}}),c=new n(`antMoveLeftIn`,{"0%":{transform:`translate3d(-100%, 0, 0)`,transformOrigin:`0 0`,opacity:0},"100%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1}}),l=new n(`antMoveLeftOut`,{"0%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1},"100%":{transform:`translate3d(-100%, 0, 0)`,transformOrigin:`0 0`,opacity:0}}),u=new n(`antMoveRightIn`,{"0%":{transform:`translate3d(100%, 0, 0)`,transformOrigin:`0 0`,opacity:0},"100%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1}}),d=new n(`antMoveRightOut`,{"0%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1},"100%":{transform:`translate3d(100%, 0, 0)`,transformOrigin:`0 0`,opacity:0}}),f={"move-up":{inKeyframes:new n(`antMoveUpIn`,{"0%":{transform:`translate3d(0, -100%, 0)`,transformOrigin:`0 0`,opacity:0},"100%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1}}),outKeyframes:new n(`antMoveUpOut`,{"0%":{transform:`translate3d(0, 0, 0)`,transformOrigin:`0 0`,opacity:1},"100%":{transform:`translate3d(0, -100%, 0)`,transformOrigin:`0 0`,opacity:0}})},"move-down":{inKeyframes:o,outKeyframes:s},"move-left":{inKeyframes:c,outKeyframes:l},"move-right":{inKeyframes:u,outKeyframes:d}},p=(e,t)=>{let{antCls:n}=e,r=`${n}-${t}`,{inKeyframes:i,outKeyframes:o}=f[t];return[a(r,i,o,e.motionDurationMid),{[`
${r}-enter,
${r}-appear
`]:{opacity:0,animationTimingFunction:e.motionEaseOutCirc},[`${r}-leave`]:{animationTimingFunction:e.motionEaseInOutCirc}}]},m=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z`}}]},name:`left`,theme:`outlined`}})),h=e(r()),g=e(m());function _(){return _=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},_.apply(this,arguments)}var v=h.forwardRef((e,t)=>h.createElement(i,_({},e,{ref:t,icon:g.default})));export{p as n,v as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z`}}]},name:`file-text`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default}))),l=e(t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M574 665.4a8.03 8.03 0 00-11.3 0L446.5 781.6c-53.8 53.8-144.6 59.5-204 0-59.5-59.5-53.8-150.2 0-204l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3l-39.8-39.8a8.03 8.03 0 00-11.3 0L191.4 526.5c-84.6 84.6-84.6 221.5 0 306s221.5 84.6 306 0l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3L574 665.4zm258.6-474c-84.6-84.6-221.5-84.6-306 0L410.3 307.6a8.03 8.03 0 000 11.3l39.7 39.7c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c53.8-53.8 144.6-59.5 204 0 59.5 59.5 53.8 150.2 0 204L665.3 562.6a8.03 8.03 0 000 11.3l39.8 39.8c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c84.5-84.6 84.5-221.5 0-306.1zM610.1 372.3a8.03 8.03 0 00-11.3 0L372.3 598.7a8.03 8.03 0 000 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l226.4-226.4c3.1-3.1 3.1-8.2 0-11.3l-39.5-39.6z`}}]},name:`link`,theme:`outlined`}}))());function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},u.apply(this,arguments)}var d=a.forwardRef((e,t)=>a.createElement(r,u({},e,{ref:t,icon:l.default})));export{c as n,d as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M820 436h-40c-4.4 0-8 3.6-8 8v40c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-40c0-4.4-3.6-8-8-8zm32-104H732V120c0-4.4-3.6-8-8-8H300c-4.4 0-8 3.6-8 8v212H172c-44.2 0-80 35.8-80 80v328c0 17.7 14.3 32 32 32h168v132c0 4.4 3.6 8 8 8h424c4.4 0 8-3.6 8-8V772h168c17.7 0 32-14.3 32-32V412c0-44.2-35.8-80-80-80zM360 180h304v152H360V180zm304 664H360V568h304v276zm200-140H732V500H292v204H160V412c0-6.6 5.4-12 12-12h680c6.6 0 12 5.4 12 12v292z`}}]},name:`printer`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
-1
View File
@@ -1 +0,0 @@
import{i as e}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as t,Ln as n,yr as r}from"./jsx-runtime-CRBytmvs.js";import{t as i}from"./config-provider-CqLGIhNd.js";var a=e(t());function o(e){return t=>a.createElement(i,{theme:{token:{motion:!1,zIndexPopupBase:0}}},a.createElement(e,{...t}))}var s=(e,t,i,s,c)=>o(o=>{let{prefixCls:l,style:u}=o,d=a.useRef(null),[f,p]=a.useState(0),[m,h]=a.useState(0),[g,_]=r(!1,o.open),{getPrefixCls:v}=a.useContext(n),y=v(s||`select`,l);a.useEffect(()=>{if(_(!0),typeof ResizeObserver<`u`){let e=new ResizeObserver(e=>{let t=e[0].target;p(t.offsetHeight+8),h(t.offsetWidth)}),t=setInterval(()=>{let n=c?`.${c(y)}`:`.${y}-dropdown`,r=d.current?.querySelector(n);r&&(clearInterval(t),e.observe(r))},10);return()=>{clearInterval(t),e.disconnect()}}},[y]);let b={...o,style:{...u,margin:0},open:g,getPopupContainer:()=>d.current};i&&(b=i(b)),t&&(b={...b,[t]:{overflow:{adjustX:!1,adjustY:!1}}});let x={paddingBottom:f,position:`relative`,minWidth:m};return a.createElement(`div`,{ref:d,style:x},a.createElement(e,{...b}))});export{o as n,s as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M824.8 613.2c-16-51.4-34.4-94.6-62.7-165.3C766.5 262.2 689.3 112 511.5 112 331.7 112 256.2 265.2 261 447.9c-28.4 70.8-46.7 113.7-62.7 165.3-34 109.5-23 154.8-14.6 155.8 18 2.2 70.1-82.4 70.1-82.4 0 49 25.2 112.9 79.8 159-26.4 8.1-85.7 29.9-71.6 53.8 11.4 19.3 196.2 12.3 249.5 6.3 53.3 6 238.1 13 249.5-6.3 14.1-23.8-45.3-45.7-71.6-53.8 54.6-46.2 79.8-110.1 79.8-159 0 0 52.1 84.6 70.1 82.4 8.5-1.1 19.5-46.4-14.5-155.8z`}}]},name:`qq`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M917 211.1l-199.2 24c-6.6.8-9.4 8.9-4.7 13.6l59.3 59.3-226 226-101.8-101.7c-6.3-6.3-16.4-6.2-22.6 0L100.3 754.1a8.03 8.03 0 000 11.3l45 45.2c3.1 3.1 8.2 3.1 11.3 0L433.3 534 535 635.7c6.3 6.2 16.4 6.2 22.6 0L829 364.5l59.3 59.3a8.01 8.01 0 0013.6-4.7l24-199.2c.7-5.1-3.7-9.5-8.9-8.8z`}}]},name:`rise`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z`}}]},name:`search`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
-1
View File
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M882 272.1V144c0-17.7-14.3-32-32-32H174c-17.7 0-32 14.3-32 32v128.1c-16.7 1-30 14.9-30 31.9v131.7a177 177 0 0014.4 70.4c4.3 10.2 9.6 19.8 15.6 28.9v345c0 17.6 14.3 32 32 32h676c17.7 0 32-14.3 32-32V535a175 175 0 0015.6-28.9c9.5-22.3 14.4-46 14.4-70.4V304c0-17-13.3-30.9-30-31.9zM214 184h596v88H214v-88zm362 656.1H448V736h128v104.1zm234 0H640V704c0-17.7-14.3-32-32-32H416c-17.7 0-32 14.3-32 32v136.1H214V597.9c2.9 1.4 5.9 2.8 9 4 22.3 9.4 46 14.1 70.4 14.1s48-4.7 70.4-14.1c13.8-5.8 26.8-13.2 38.7-22.1.2-.1.4-.1.6 0a180.4 180.4 0 0038.7 22.1c22.3 9.4 46 14.1 70.4 14.1 24.4 0 48-4.7 70.4-14.1 13.8-5.8 26.8-13.2 38.7-22.1.2-.1.4-.1.6 0a180.4 180.4 0 0038.7 22.1c22.3 9.4 46 14.1 70.4 14.1 24.4 0 48-4.7 70.4-14.1 3-1.3 6-2.6 9-4v242.2zm30-404.4c0 59.8-49 108.3-109.3 108.3-40.8 0-76.4-22.1-95.2-54.9-2.9-5-8.1-8.1-13.9-8.1h-.6c-5.7 0-11 3.1-13.9 8.1A109.24 109.24 0 01512 544c-40.7 0-76.2-22-95-54.7-3-5.1-8.4-8.3-14.3-8.3s-11.4 3.2-14.3 8.3a109.63 109.63 0 01-95.1 54.7C233 544 184 495.5 184 435.7v-91.2c0-.3.2-.5.5-.5h655c.3 0 .5.2.5.5v91.2z`}}]},name:`shop`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
@@ -1 +0,0 @@
import{i as e,t}from"./rolldown-runtime-BgaNhQyE.js";import{Dr as n,Ft as r}from"./jsx-runtime-CRBytmvs.js";var i=t((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M832 312H696v-16c0-101.6-82.4-184-184-184s-184 82.4-184 184v16H192c-17.7 0-32 14.3-32 32v536c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V344c0-17.7-14.3-32-32-32zm-432-16c0-61.9 50.1-112 112-112s112 50.1 112 112v16H400v-16zm392 544H232V384h96v88c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-88h224v88c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-88h96v456z`}}]},name:`shopping`,theme:`outlined`}})),a=e(n()),o=e(i());function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s.apply(this,arguments)}var c=a.forwardRef((e,t)=>a.createElement(r,s({},e,{ref:t,icon:o.default})));export{c as t};
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More