318 lines
11 KiB
PHP
318 lines
11 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Service\business;
|
|||
|
|
|
|||
|
|
use App\BaseApp\BaseService;
|
|||
|
|
use App\Models\business\CatalogueModel;
|
|||
|
|
use App\Models\business\PackageItemModel;
|
|||
|
|
use App\Models\business\PackageModel;
|
|||
|
|
use App\Models\business\PriceSheetModel;
|
|||
|
|
use App\Service\common\MediaUrlService;
|
|||
|
|
use Illuminate\Support\Facades\DB;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 后台套餐
|
|||
|
|
*
|
|||
|
|
* 明细必须带规格。保存明细时后端重算原价;套餐报价若前端没传,就按原价差额自动补。
|
|||
|
|
*/
|
|||
|
|
class PackageService extends BaseService
|
|||
|
|
{
|
|||
|
|
private MediaUrlService $media;
|
|||
|
|
|
|||
|
|
private PackageQuoteService $quote;
|
|||
|
|
|
|||
|
|
private PriceService $price;
|
|||
|
|
|
|||
|
|
public function __construct()
|
|||
|
|
{
|
|||
|
|
parent::__construct();
|
|||
|
|
$this->model = PackageModel::class;
|
|||
|
|
$this->selectField = [
|
|||
|
|
'id', 'name', 'cover', 'subtitle', 'remark',
|
|||
|
|
'original_amount', 'package_amount', 'is_hot', 'sort', 'status',
|
|||
|
|
'created_at', 'updated_at',
|
|||
|
|
];
|
|||
|
|
$this->queryField = [
|
|||
|
|
'name' => 'like',
|
|||
|
|
'status' => '=',
|
|||
|
|
'is_hot' => '=',
|
|||
|
|
];
|
|||
|
|
$this->orderBy = ['name' => 'sort', 'sort' => 'asc'];
|
|||
|
|
$this->media = MediaUrlService::getInstance();
|
|||
|
|
$this->quote = PackageQuoteService::getInstance();
|
|||
|
|
$this->price = PriceService::getInstance();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function list(): array
|
|||
|
|
{
|
|||
|
|
$result = $this->getPageList();
|
|||
|
|
$ids = array_column($result['items'], 'id');
|
|||
|
|
$counts = PackageItemModel::whereIn('package_id', $ids)
|
|||
|
|
->where('deleted_at', 0)
|
|||
|
|
->selectRaw('package_id, count(*) as total')
|
|||
|
|
->groupBy('package_id')
|
|||
|
|
->get()
|
|||
|
|
->keyBy('package_id');
|
|||
|
|
foreach ($result['items'] as &$item) {
|
|||
|
|
$item['cover'] = $this->media->toPublic($item['cover'] ?? '');
|
|||
|
|
$item['item_count'] = (int) ($counts[$item['id']]['total'] ?? 0);
|
|||
|
|
$item = $this->withAmountText($item);
|
|||
|
|
}
|
|||
|
|
unset($item);
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function option(): mixed
|
|||
|
|
{
|
|||
|
|
$this->optionField = ['id', 'name'];
|
|||
|
|
return $this->getOption();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function detail($id): mixed
|
|||
|
|
{
|
|||
|
|
$info = PackageModel::with([
|
|||
|
|
'items' => fn ($query) => $query->where('deleted_at', 0)->orderBy('sort')->orderBy('id'),
|
|||
|
|
'items.catalogue',
|
|||
|
|
'items.priceSheet',
|
|||
|
|
])->where('id', $id)->where('deleted_at', 0)->first();
|
|||
|
|
if (empty($info)) {
|
|||
|
|
$this->utils->errorThrow('套餐不存在');
|
|||
|
|
}
|
|||
|
|
$info = $info->toArray();
|
|||
|
|
$info['cover'] = $this->media->toPublic($info['cover'] ?? '');
|
|||
|
|
foreach ($info['items'] as &$item) {
|
|||
|
|
$item['title'] = $item['catalogue']['title'] ?? '';
|
|||
|
|
$item['cover'] = $this->media->toPublic($item['catalogue']['cover'] ?? '');
|
|||
|
|
$item['specification'] = $item['price_sheet']['specification'] ?? '';
|
|||
|
|
$item['dimension'] = $item['price_sheet']['dimension'] ?? '';
|
|||
|
|
$item['unit_price_text'] = $this->price->centsToYuan((int) ($item['unit_price'] ?? 0));
|
|||
|
|
}
|
|||
|
|
unset($item);
|
|||
|
|
return $this->withAmountText($info);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function create($params): mixed
|
|||
|
|
{
|
|||
|
|
$row = $this->normalizeMain($params);
|
|||
|
|
$row['created_at'] = time();
|
|||
|
|
$id = PackageModel::insertGetId($row);
|
|||
|
|
if (!empty($params['items']) && is_array($params['items'])) {
|
|||
|
|
$this->replaceItems((int) $id, $params['items'], $params);
|
|||
|
|
}
|
|||
|
|
return $this->detail($id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function update($id, $params): mixed
|
|||
|
|
{
|
|||
|
|
$row = $this->normalizeMain($params, (int) $id);
|
|||
|
|
$this->save($id, $row);
|
|||
|
|
return $this->detail($id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function delete($ids): mixed
|
|||
|
|
{
|
|||
|
|
$ids = (array) $ids;
|
|||
|
|
$now = time();
|
|||
|
|
PackageModel::whereIn('id', $ids)->update(['deleted_at' => $now, 'updated_at' => $now]);
|
|||
|
|
PackageItemModel::whereIn('package_id', $ids)->update(['deleted_at' => $now, 'updated_at' => $now]);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function status($id, $status): mixed
|
|||
|
|
{
|
|||
|
|
return $this->save($id, ['status' => (int) $status]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 搭配搜索:商品封面 + 规格报价。
|
|||
|
|
* 单价用和保存时同一套 resolveUnitPrice,没有数字报价的规格标成不可选,避免搜到却存不进去。
|
|||
|
|
*/
|
|||
|
|
public function searchCatalog(): array
|
|||
|
|
{
|
|||
|
|
$keyword = trim((string) request()->input('keyword', ''));
|
|||
|
|
$limit = max(1, min(50, (int) request()->input('limit', 20)));
|
|||
|
|
|
|||
|
|
$query = CatalogueModel::query()
|
|||
|
|
->where('deleted_at', 0)
|
|||
|
|
->with([
|
|||
|
|
'priceSheet' => fn ($q) => $q->where('deleted_at', 0)->orderBy('id'),
|
|||
|
|
]);
|
|||
|
|
if ($keyword !== '') {
|
|||
|
|
$like = '%' . $keyword . '%';
|
|||
|
|
$query->where(function ($inner) use ($like) {
|
|||
|
|
$inner->where('title', 'like', $like)
|
|||
|
|
->orWhere('identifier', 'like', $like)
|
|||
|
|
->orWhere('alias', 'like', $like);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$rows = $query->orderByDesc('id')->limit($limit)->get()->toArray();
|
|||
|
|
$items = [];
|
|||
|
|
foreach ($rows as $row) {
|
|||
|
|
$sheets = [];
|
|||
|
|
foreach ($row['price_sheet'] ?? [] as $sheet) {
|
|||
|
|
$unit = $this->price->resolveUnitPrice((string) ($sheet['routine'] ?? ''), 'routine', 1);
|
|||
|
|
$sheets[] = [
|
|||
|
|
'id' => (int) ($sheet['id'] ?? 0),
|
|||
|
|
'specification' => (string) ($sheet['specification'] ?? ''),
|
|||
|
|
'dimension' => (string) ($sheet['dimension'] ?? ''),
|
|||
|
|
'unit_price' => $unit,
|
|||
|
|
'unit_price_text' => $this->price->centsToYuan($unit),
|
|||
|
|
'selectable' => $unit > 0,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
$items[] = [
|
|||
|
|
'id' => (int) ($row['id'] ?? 0),
|
|||
|
|
'title' => (string) ($row['title'] ?? ''),
|
|||
|
|
'identifier' => (string) ($row['identifier'] ?? ''),
|
|||
|
|
'cover' => $this->media->toPublic($row['cover'] ?? ''),
|
|||
|
|
'sheets' => $sheets,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
return ['items' => $items];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 整表替换搭配明细,并按原价差额补套餐报价
|
|||
|
|
*/
|
|||
|
|
public function saveItems(array $params): mixed
|
|||
|
|
{
|
|||
|
|
$id = (int) ($params['id'] ?? 0);
|
|||
|
|
if ($id <= 0) {
|
|||
|
|
$this->utils->errorThrow('请选择套餐');
|
|||
|
|
}
|
|||
|
|
$pkg = PackageModel::where('id', $id)->where('deleted_at', 0)->first();
|
|||
|
|
if (empty($pkg)) {
|
|||
|
|
$this->utils->errorThrow('套餐不存在');
|
|||
|
|
}
|
|||
|
|
$this->replaceItems($id, $params['items'] ?? [], $params);
|
|||
|
|
return $this->detail($id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清洗主表字段。package_amount 按元进来,转成分;没传报价时新建默认等于原价。
|
|||
|
|
*/
|
|||
|
|
private function normalizeMain(array $params, int $id = 0): array
|
|||
|
|
{
|
|||
|
|
$row = [
|
|||
|
|
'updated_at' => time(),
|
|||
|
|
];
|
|||
|
|
if (array_key_exists('name', $params)) {
|
|||
|
|
$name = trim((string) $params['name']);
|
|||
|
|
if ($name === '') {
|
|||
|
|
$this->utils->errorThrow('请填写套餐名称');
|
|||
|
|
}
|
|||
|
|
$row['name'] = $name;
|
|||
|
|
} elseif ($id === 0) {
|
|||
|
|
$this->utils->errorThrow('请填写套餐名称');
|
|||
|
|
}
|
|||
|
|
if (array_key_exists('cover', $params)) {
|
|||
|
|
$row['cover'] = $this->media->firstOf($params['cover']);
|
|||
|
|
}
|
|||
|
|
foreach (['subtitle', 'remark'] as $field) {
|
|||
|
|
if (array_key_exists($field, $params)) {
|
|||
|
|
$row[$field] = (string) $params[$field];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
foreach (['is_hot', 'sort', 'status'] as $field) {
|
|||
|
|
if (array_key_exists($field, $params)) {
|
|||
|
|
$row[$field] = (int) $params[$field];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (array_key_exists('package_amount', $params)) {
|
|||
|
|
$row['package_amount'] = max(0, $this->price->yuanToCents($params['package_amount']));
|
|||
|
|
}
|
|||
|
|
if ($id === 0 && !isset($row['package_amount'])) {
|
|||
|
|
$row['package_amount'] = 0;
|
|||
|
|
$row['original_amount'] = 0;
|
|||
|
|
}
|
|||
|
|
return $row;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 校验规格、写入明细、重算原价。前端传了 package_amount(元)就用它,否则按差额补。
|
|||
|
|
*/
|
|||
|
|
private function replaceItems(int $packageId, array $items, array $params): void
|
|||
|
|
{
|
|||
|
|
$pkg = PackageModel::where('id', $packageId)->where('deleted_at', 0)->first();
|
|||
|
|
$oldOriginal = (int) ($pkg['original_amount'] ?? 0);
|
|||
|
|
$oldQuote = (int) ($pkg['package_amount'] ?? 0);
|
|||
|
|
|
|||
|
|
$rows = [];
|
|||
|
|
$now = time();
|
|||
|
|
$sort = 0;
|
|||
|
|
$newOriginal = 0;
|
|||
|
|
foreach ($items as $item) {
|
|||
|
|
if (!is_array($item)) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$catalogueId = (int) ($item['catalogue_id'] ?? 0);
|
|||
|
|
$sheetId = (int) ($item['price_sheet_id'] ?? 0);
|
|||
|
|
$qty = max(1, (int) ($item['quantity'] ?? 1));
|
|||
|
|
if ($catalogueId <= 0 || $sheetId <= 0) {
|
|||
|
|
$this->utils->errorThrow('每件商品都必须选择规格');
|
|||
|
|
}
|
|||
|
|
$sheet = PriceSheetModel::where('id', $sheetId)
|
|||
|
|
->where('catalogue_id', $catalogueId)
|
|||
|
|
->where('deleted_at', 0)
|
|||
|
|
->first();
|
|||
|
|
if (empty($sheet)) {
|
|||
|
|
$this->utils->errorThrow('规格不存在或不属于该商品');
|
|||
|
|
}
|
|||
|
|
$unit = $this->price->resolveUnitPrice(
|
|||
|
|
(string) ($sheet['routine'] ?? ''),
|
|||
|
|
(string) ($item['material_key'] ?? 'routine'),
|
|||
|
|
1
|
|||
|
|
);
|
|||
|
|
if ($unit <= 0) {
|
|||
|
|
$this->utils->errorThrow('规格缺少有效报价');
|
|||
|
|
}
|
|||
|
|
$newOriginal += $unit * $qty;
|
|||
|
|
$rows[] = [
|
|||
|
|
'package_id' => $packageId,
|
|||
|
|
'catalogue_id' => $catalogueId,
|
|||
|
|
'price_sheet_id' => $sheetId,
|
|||
|
|
'material_key' => (string) ($item['material_key'] ?? 'routine'),
|
|||
|
|
'quantity' => $qty,
|
|||
|
|
'unit_price' => $unit,
|
|||
|
|
'sort' => (int) ($item['sort'] ?? $sort),
|
|||
|
|
'remark' => (string) ($item['remark'] ?? ''),
|
|||
|
|
'created_at' => $now,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
'deleted_at' => 0,
|
|||
|
|
];
|
|||
|
|
$sort++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$quote = array_key_exists('package_amount', $params)
|
|||
|
|
? max(0, $this->price->yuanToCents($params['package_amount']))
|
|||
|
|
: $this->quote->applyOriginalDiff($oldOriginal, $newOriginal, $oldQuote);
|
|||
|
|
|
|||
|
|
DB::connection('business')->transaction(function () use ($packageId, $rows, $now, $newOriginal, $quote) {
|
|||
|
|
PackageItemModel::where('package_id', $packageId)->where('deleted_at', 0)->update([
|
|||
|
|
'deleted_at' => $now,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
]);
|
|||
|
|
if (!empty($rows)) {
|
|||
|
|
PackageItemModel::insert($rows);
|
|||
|
|
}
|
|||
|
|
PackageModel::where('id', $packageId)->update([
|
|||
|
|
'original_amount' => $newOriginal,
|
|||
|
|
'package_amount' => $quote,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 补原价/套餐价/优惠的元文字段
|
|||
|
|
*/
|
|||
|
|
private function withAmountText(array $row): array
|
|||
|
|
{
|
|||
|
|
$original = (int) ($row['original_amount'] ?? 0);
|
|||
|
|
$quote = (int) ($row['package_amount'] ?? 0);
|
|||
|
|
$row['discount_amount'] = max(0, $original - $quote);
|
|||
|
|
return $this->quote->withText($row);
|
|||
|
|
}
|
|||
|
|
}
|