model = WxTemplateModel::class; $this->selectField = [ 'id', 'name', 'code', 'preview', 'style_tag', 'is_default', 'status', 'app_code', 'version', 'sort', 'created_at', 'updated_at', ]; $this->queryField = [ 'name' => 'like', 'code' => 'like', 'style_tag' => '=', 'app_code' => '=', 'status' => '=', ]; $this->orderBy = ['name' => 'sort', 'sort' => 'asc']; } public function list(): array { $result = $this->getPageList(); // 列表不回完整 tokens(体积大),但卡片预览需要色板与布局摘要 $ids = array_values(array_filter(array_map( static fn ($row) => (int) ($row['id'] ?? 0), $result['items'] ?? [] ))); if ($ids === []) { return $result; } $extras = WxTemplateModel::whereIn('id', $ids) ->get(['id', 'tokens', 'layout']) ->keyBy('id'); foreach ($result['items'] as &$item) { $extra = $extras[(int) $item['id']] ?? null; $tokens = is_array($extra?->tokens) ? $extra->tokens : []; $layout = is_array($extra?->layout) ? $extra->layout : []; $color = is_array($tokens['color'] ?? null) ? $tokens['color'] : []; $home = is_array($layout['home'] ?? null) ? $layout['home'] : []; $item['swatch'] = [ 'primary' => (string) ($color['primary'] ?? '#B08D57'), 'accent' => (string) ($color['accent'] ?? ($color['primary'] ?? '#B08D57')), 'bg' => (string) ($color['bg'] ?? '#FAFAF9'), 'surface' => (string) ($color['surface'] ?? '#FFFFFF'), 'text' => (string) ($color['text'] ?? '#18181B'), 'border' => (string) ($color['border'] ?? '#E4E4E7'), ]; $item['layout_hint'] = [ 'hero' => (string) ($home['hero'] ?? 'banner'), 'category' => (string) ($home['category'] ?? 'grid'), 'product' => (string) ($home['product'] ?? 'grid'), ]; } unset($item); return $result; } public function option(): mixed { $this->optionField = ['id', 'name', 'code', 'style_tag']; return $this->getOption(); } public function detail($id): mixed { // 详情是单条记录,没有 list 的字段裁剪压力; // tokens/layout 必须回显(编辑弹窗回填要用),不能用默认 selectField(它把这两个字段排除了)。 $saved = $this->selectField; $this->selectField = ['*']; try { return $this->getDetail($id); } finally { $this->selectField = $saved; } } public function create($params): mixed { $params = $this->normalize($params); return $this->insert($params); } public function update($id, $params): mixed { $params = $this->normalize($params, (int) $id); return $this->save($id, $params); } public function delete($ids): mixed { // 默认模板被删掉小程序就没样式可用了,必须先改默认再删 $hasDefault = WxTemplateModel::whereIn('id', (array) $ids)->where('is_default', 1)->exists(); if ($hasDefault) { $this->utils->errorThrow('默认模板不能删除,请先把其他模板设为默认'); } return $this->del($ids); } public function status($id, $status): mixed { return $this->save($id, ['status' => (int) $status]); } /** * 设为默认(同一 app_code 下只能有一个默认) * 同时把 code 写入 nl_wx_app.template_code,保证小程序 current() 优先命中 */ public function setDefault(int $id): bool { $template = WxTemplateModel::where('id', $id)->where('deleted_at', 0)->first(); if (empty($template)) { $this->utils->errorThrow('模板不存在'); } DB::connection('business')->transaction(function () use ($template, $id) { WxTemplateModel::where('app_code', $template['app_code']) ->where('id', '!=', $id) ->update(['is_default' => 0, 'updated_at' => time()]); WxTemplateModel::where('id', $id)->update([ 'is_default' => 1, 'status' => 0, 'updated_at' => time(), ]); }); // 系统库 nl_wx_app:空 app_code 的全局模板同步到全部启用应用;有品牌则只同步该品牌 $appQuery = \App\Models\WxAppModel::where('deleted_at', 0)->where('status', 0); $appCode = trim((string) ($template['app_code'] ?? '')); if ($appCode !== '') { $appQuery->where('code', $appCode); } $appQuery->update([ 'template_code' => (string) $template['code'], 'updated_at' => time(), ]); return true; } /** * 导出:给出可直接再导入的 JSON */ public function export(array $ids): array { $rows = WxTemplateModel::whereIn('id', $ids)->where('deleted_at', 0)->get([ 'name', 'code', 'preview', 'style_tag', 'tokens', 'layout', 'app_code', 'version', ]); return [ 'version' => 1, 'exported_at' => date('Y-m-d H:i:s'), 'templates' => $rows->toArray(), ]; } /** * 导入:整包校验通过才落库 * * 单条不合法就整包拒绝,不做「部分成功」——一半新一半旧的模板库更难排查。 */ public function import(array $payload, bool $overwrite = false): array { $templates = $payload['templates'] ?? $payload; if (!is_array($templates) || empty($templates)) { $this->utils->errorThrow('导入内容为空'); } $schema = WxTemplateSchemaService::getInstance(); $rows = []; foreach ($templates as $index => $item) { $code = trim((string) ($item['code'] ?? '')); $name = trim((string) ($item['name'] ?? '')); if ($code === '' || $name === '') { $this->utils->errorThrow('第 ' . ($index + 1) . ' 个模板缺少 code 或 name'); } $rows[] = [ 'code' => $code, 'name' => $name, 'preview' => (string) ($item['preview'] ?? ''), 'style_tag' => (string) ($item['style_tag'] ?? ''), 'app_code' => (string) ($item['app_code'] ?? ''), 'tokens' => json_encode($schema->sanitizeTokens($item['tokens'] ?? [], true), JSON_UNESCAPED_UNICODE), 'layout' => json_encode($schema->sanitizeLayout($item['layout'] ?? [], true), JSON_UNESCAPED_UNICODE), 'version' => max(1, (int) ($item['version'] ?? 1)), ]; } $inserted = 0; $updated = 0; $skipped = 0; DB::connection('business')->transaction(function () use ($rows, $overwrite, &$inserted, &$updated, &$skipped) { foreach ($rows as $row) { $exists = WxTemplateModel::where('code', $row['code']) ->where('app_code', $row['app_code']) ->first(); if (!empty($exists)) { if (!$overwrite) { $skipped++; continue; } $row['version'] = (int) $exists['version'] + 1; $row['updated_at'] = time(); WxTemplateModel::where('id', $exists['id'])->update($row); $updated++; continue; } $row['created_at'] = time(); WxTemplateModel::insert($row); $inserted++; } }); return ['inserted' => $inserted, 'updated' => $updated, 'skipped' => $skipped]; } /** * 用内置预设初始化模板库(20 套) */ public function initPresets(bool $overwrite = false): array { $presets = WxTemplatePresetService::getInstance()->all(); $result = $this->import(['templates' => $presets], $overwrite); // 一套默认都没有的话,把第一套轻奢设为默认 if (!WxTemplateModel::where('deleted_at', 0)->where('is_default', 1)->exists()) { $first = WxTemplateModel::where('deleted_at', 0)->orderBy('sort', 'asc')->first(); if (!empty($first)) { $this->setDefault((int) $first['id']); } } return $result; } /** * 入库前清洗;code 在同一品牌下唯一 */ private function normalize(array $params, int $excludeId = 0): array { $schema = WxTemplateSchemaService::getInstance(); if (array_key_exists('tokens', $params)) { $params['tokens'] = json_encode($schema->sanitizeTokens($params['tokens'], true), JSON_UNESCAPED_UNICODE); } if (array_key_exists('layout', $params)) { $params['layout'] = json_encode($schema->sanitizeLayout($params['layout'], true), JSON_UNESCAPED_UNICODE); } if (!empty($params['code'])) { $duplicate = WxTemplateModel::where('code', $params['code']) ->where('app_code', (string) ($params['app_code'] ?? '')) ->when($excludeId > 0, fn ($query) => $query->where('id', '!=', $excludeId)) ->exists(); if ($duplicate) { $this->utils->errorThrow('模板标识已存在'); } } return $params; } }