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('套餐还没有搭配商品'); } $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); } }