2026-08-19 08:16:49 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Service\wx;
|
|
|
|
|
|
|
|
|
|
|
|
use App\BaseApp\BaseWxService;
|
|
|
|
|
|
use App\Models\business\ListItemModel;
|
|
|
|
|
|
use App\Models\business\ListModel;
|
|
|
|
|
|
use App\Models\business\PackageItemModel;
|
|
|
|
|
|
use App\Models\business\PackageModel;
|
|
|
|
|
|
use App\Service\business\PackageQuoteService;
|
|
|
|
|
|
use App\Service\business\PriceService;
|
|
|
|
|
|
use App\Service\business\SerialNoService;
|
|
|
|
|
|
use App\Service\common\MediaUrlService;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 小程序套餐:热门/列表/详情免登录,一件加入清单要登录
|
|
|
|
|
|
*
|
|
|
|
|
|
* 应用没开套餐时列表直接空,避免关了开关首页还露出卡片。
|
|
|
|
|
|
*/
|
|
|
|
|
|
class WxPackageService extends BaseWxService
|
|
|
|
|
|
{
|
|
|
|
|
|
protected bool $needLogin = false;
|
|
|
|
|
|
|
|
|
|
|
|
private MediaUrlService $media;
|
|
|
|
|
|
|
|
|
|
|
|
private PackageQuoteService $quote;
|
|
|
|
|
|
|
|
|
|
|
|
private PriceService $price;
|
|
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
|
{
|
|
|
|
|
|
parent::__construct();
|
|
|
|
|
|
$this->media = MediaUrlService::getInstance();
|
|
|
|
|
|
$this->quote = PackageQuoteService::getInstance();
|
|
|
|
|
|
$this->price = PriceService::getInstance();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 首页热门
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function hot(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!$this->featureEnabled()) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
$rows = PackageModel::where('deleted_at', 0)
|
|
|
|
|
|
->where('status', 0)
|
|
|
|
|
|
->where('is_hot', 1)
|
|
|
|
|
|
->orderBy('sort')
|
|
|
|
|
|
->orderBy('id', 'desc')
|
|
|
|
|
|
->get();
|
|
|
|
|
|
return $this->mapCards($rows->toArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 套餐 tab 列表;keyword / name 按名称、副标题模糊查
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function list(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!$this->featureEnabled()) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
$page = max(1, (int) request()->get('page', 1));
|
|
|
|
|
|
$pageSize = max(1, (int) request()->get('pageSize', 20));
|
|
|
|
|
|
$keyword = trim((string) request()->get('keyword', request()->get('name', '')));
|
|
|
|
|
|
$query = PackageModel::where('deleted_at', 0)->where('status', 0)->orderBy('sort')->orderBy('id', 'desc');
|
|
|
|
|
|
if ($keyword !== '') {
|
|
|
|
|
|
$query->where(function ($inner) use ($keyword) {
|
|
|
|
|
|
$inner->where('name', 'like', '%' . $keyword . '%')
|
|
|
|
|
|
->orWhere('subtitle', 'like', '%' . $keyword . '%');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
$total = (clone $query)->count();
|
|
|
|
|
|
$rows = $query->forPage($page, $pageSize)->get()->toArray();
|
|
|
|
|
|
return [
|
|
|
|
|
|
'items' => $this->mapCards($rows),
|
|
|
|
|
|
'total' => $total,
|
|
|
|
|
|
'page' => $page,
|
|
|
|
|
|
'pageSize' => $pageSize,
|
|
|
|
|
|
'has_more' => ($page * $pageSize) < $total,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 套餐详情(含搭配与价格)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 带 p_user_id 时与商品详情同一套:代理商分享进来的普通用户继承倍率。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function detail(int $id, int $shareUserId = 0): array
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($shareUserId > 0) {
|
|
|
|
|
|
$this->inheritAgentPrice($shareUserId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!$this->featureEnabled()) {
|
|
|
|
|
|
$this->utils->errorThrow('套餐功能未启用');
|
|
|
|
|
|
}
|
|
|
|
|
|
$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)->where('status', 0)->first();
|
|
|
|
|
|
if (empty($info)) {
|
|
|
|
|
|
$this->utils->errorThrow('套餐不存在或已下架');
|
|
|
|
|
|
}
|
|
|
|
|
|
$info = $info->toArray();
|
|
|
|
|
|
$show = $this->canSeePrice();
|
|
|
|
|
|
$multiplier = $this->priceMultiplier();
|
|
|
|
|
|
$info['cover'] = $this->media->toPublic($info['cover'] ?? '');
|
|
|
|
|
|
$info['item_count'] = count($info['items'] ?? []);
|
|
|
|
|
|
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'] ?? '';
|
|
|
|
|
|
$unit = $this->quote->applyMultiplier((int) ($item['unit_price'] ?? 0), $multiplier);
|
|
|
|
|
|
$item['unit_price'] = $unit;
|
|
|
|
|
|
$item['unit_price_text'] = $show ? $this->price->centsToYuan($unit) : PriceService::MASK;
|
|
|
|
|
|
$item['product'] = $item['catalogue'] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
unset($item);
|
|
|
|
|
|
return $this->withWxAmounts($info, $show, $multiplier);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 一件加入清单:记下套餐价/原价和每行快照,供以后补差价
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function toList(array $params): array
|
|
|
|
|
|
{
|
|
|
|
|
|
// 写操作必须登录,构造时 needLogin=false 是为了列表免鉴权
|
|
|
|
|
|
if ($this->userId <= 0) {
|
|
|
|
|
|
$this->utils->errorThrow('请先登录');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!$this->featureEnabled()) {
|
|
|
|
|
|
$this->utils->errorThrow('套餐功能未启用');
|
|
|
|
|
|
}
|
|
|
|
|
|
$packageId = (int) ($params['package_id'] ?? 0);
|
|
|
|
|
|
$detail = $this->detail($packageId);
|
|
|
|
|
|
$items = $detail['items'] ?? [];
|
|
|
|
|
|
if (empty($items)) {
|
|
|
|
|
|
$this->utils->errorThrow('套餐还没有搭配商品');
|
|
|
|
|
|
}
|
2026-08-20 19:16:49 +08:00
|
|
|
|
$stock = \App\Service\business\StockService::getInstance();
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$stock->assertAvailable(
|
|
|
|
|
|
(int) ($item['price_sheet_id'] ?? 0),
|
|
|
|
|
|
max(1, (int) ($item['quantity'] ?? 1))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-08-19 08:16:49 +08:00
|
|
|
|
|
|
|
|
|
|
$listId = (int) ($params['list_id'] ?? 0);
|
|
|
|
|
|
$list = null;
|
|
|
|
|
|
if ($listId > 0) {
|
|
|
|
|
|
$list = ListModel::where('id', $listId)
|
|
|
|
|
|
->where('user_id', $this->userId)
|
|
|
|
|
|
->where('deleted_at', 0)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
if (empty($list)) {
|
|
|
|
|
|
$this->utils->errorThrow('清单不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
$bound = (int) ($list['package_id'] ?? 0);
|
|
|
|
|
|
if ($bound > 0 && $bound !== $packageId) {
|
|
|
|
|
|
$this->utils->errorThrow('该清单已绑定其他套餐,请新建清单');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// detail() 已经乘过用户倍率,这里直接快照,不能再乘一次
|
|
|
|
|
|
$packageAmount = (int) ($detail['package_amount'] ?? 0);
|
|
|
|
|
|
$originalAmount = (int) ($detail['original_amount'] ?? 0);
|
|
|
|
|
|
|
|
|
|
|
|
$newListId = 0;
|
|
|
|
|
|
DB::connection('business')->transaction(function () use (
|
|
|
|
|
|
&$newListId, $list, $packageId, $packageAmount, $originalAmount, $items, $detail
|
|
|
|
|
|
) {
|
|
|
|
|
|
$now = time();
|
|
|
|
|
|
if (empty($list)) {
|
|
|
|
|
|
$newListId = ListModel::insertGetId([
|
|
|
|
|
|
'list_no' => SerialNoService::getInstance()->generate(SerialNoService::PREFIX_LIST, 'list', 'list_no'),
|
|
|
|
|
|
'user_id' => $this->userId,
|
|
|
|
|
|
'name' => (string) ($detail['name'] ?? '套餐'),
|
|
|
|
|
|
'remark' => '',
|
|
|
|
|
|
'package_id' => $packageId,
|
|
|
|
|
|
'package_amount' => $packageAmount,
|
|
|
|
|
|
'original_amount' => $originalAmount,
|
|
|
|
|
|
'created_at' => $now,
|
|
|
|
|
|
]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$newListId = (int) $list['id'];
|
|
|
|
|
|
$update = ['updated_at' => $now];
|
|
|
|
|
|
if ((int) ($list['package_id'] ?? 0) === 0) {
|
|
|
|
|
|
$update['package_id'] = $packageId;
|
|
|
|
|
|
$update['package_amount'] = $packageAmount;
|
|
|
|
|
|
$update['original_amount'] = $originalAmount;
|
|
|
|
|
|
}
|
|
|
|
|
|
ListModel::where('id', $newListId)->update($update);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$catalogueId = (int) ($item['catalogue_id'] ?? 0);
|
|
|
|
|
|
$sheetId = (int) ($item['price_sheet_id'] ?? 0);
|
|
|
|
|
|
$qty = max(1, (int) ($item['quantity'] ?? 1));
|
|
|
|
|
|
$unit = (int) ($item['unit_price'] ?? 0);
|
|
|
|
|
|
$exists = ListItemModel::where('list_id', $newListId)
|
|
|
|
|
|
->where('catalogue_id', $catalogueId)
|
|
|
|
|
|
->where('price_sheet_id', $sheetId)
|
|
|
|
|
|
->where('deleted_at', 0)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
if (!empty($exists)) {
|
|
|
|
|
|
ListItemModel::where('id', $exists['id'])->update([
|
|
|
|
|
|
'quantity' => (int) $exists['quantity'] + $qty,
|
|
|
|
|
|
'unit_price' => $unit,
|
|
|
|
|
|
'package_item_id' => (int) ($item['id'] ?? 0),
|
|
|
|
|
|
'updated_at' => $now,
|
|
|
|
|
|
]);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
ListItemModel::insert([
|
|
|
|
|
|
'list_id' => $newListId,
|
|
|
|
|
|
'catalogue_id' => $catalogueId,
|
|
|
|
|
|
'price_sheet_id' => $sheetId,
|
|
|
|
|
|
'material_key' => (string) ($item['material_key'] ?? 'routine'),
|
|
|
|
|
|
'quantity' => $qty,
|
|
|
|
|
|
'unit_price' => $unit,
|
|
|
|
|
|
'package_item_id' => (int) ($item['id'] ?? 0),
|
|
|
|
|
|
'snapshot_unit_price' => $unit,
|
|
|
|
|
|
'snapshot_quantity' => $qty,
|
|
|
|
|
|
'remark' => (string) ($item['remark'] ?? ''),
|
|
|
|
|
|
'created_at' => $now,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return ['list_id' => $newListId];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 当前品牌是否打开套餐
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function featureEnabled(): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
return WxAppService::getInstance()->packageEnabled();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 卡片列表:封面、件数、有价权才带价格
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function mapCards(array $rows): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$ids = array_column($rows, 'id');
|
|
|
|
|
|
$counts = PackageItemModel::whereIn('package_id', $ids)
|
|
|
|
|
|
->where('deleted_at', 0)
|
|
|
|
|
|
->selectRaw('package_id, count(*) as total')
|
|
|
|
|
|
->groupBy('package_id')
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->keyBy('package_id');
|
|
|
|
|
|
$show = $this->canSeePrice();
|
|
|
|
|
|
$multiplier = $this->priceMultiplier();
|
|
|
|
|
|
foreach ($rows as &$row) {
|
|
|
|
|
|
$row['cover'] = $this->media->toPublic($row['cover'] ?? '');
|
|
|
|
|
|
$row['item_count'] = (int) ($counts[$row['id']]['total'] ?? 0);
|
|
|
|
|
|
$row = $this->withWxAmounts($row, $show, $multiplier);
|
|
|
|
|
|
unset($row['remark']);
|
|
|
|
|
|
}
|
|
|
|
|
|
unset($row);
|
|
|
|
|
|
return $rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把基准价乘倍率,并按价权决定是否打码
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function withWxAmounts(array $row, bool $show, mixed $multiplier): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$original = $this->quote->applyMultiplier((int) ($row['original_amount'] ?? 0), $multiplier);
|
|
|
|
|
|
$package = $this->quote->applyMultiplier((int) ($row['package_amount'] ?? 0), $multiplier);
|
|
|
|
|
|
$row['original_amount'] = $original;
|
|
|
|
|
|
$row['package_amount'] = $package;
|
|
|
|
|
|
$row['discount_amount'] = max(0, $original - $package);
|
|
|
|
|
|
$row['is_show_price'] = $show;
|
|
|
|
|
|
return $this->quote->withText($row, $show);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|