diff --git a/apps/mini-user/src/lib/wechat-share.ts b/apps/mini-user/src/lib/wechat-share.ts index a709f09..a9a4994 100644 --- a/apps/mini-user/src/lib/wechat-share.ts +++ b/apps/mini-user/src/lib/wechat-share.ts @@ -1,21 +1,126 @@ import Taro from '@tarojs/taro'; import type { WechatShareData } from '@dukang/weixin-sdk'; import { getWechatShareLink, isWechatBrowser, isMiniProgram } from '@dukang/weixin-sdk'; +import { + applyShareTitleTemplate, + DEFAULT_SHARE_DESC, + DEFAULT_SHARE_HINT, + DEFAULT_SHARE_TITLE, + resolveMiniShareRuntime, + type ClientRuntimeConfig, + type MiniShareRuntime, + type MiniShareSceneConfig, +} from '@dukang/shared-types'; import { toast } from './api'; import { getBrandAssetsSync, loadBrandAssets } from './brand-assets'; +import { fetchClientConfig } from './pay-wechat'; import { isWechatEnv, weixinSdk } from './weixin'; -export const DEFAULT_SHARE_TITLE = '你吃饭,我买单'; -export const DEFAULT_SHARE_DESC = '杜康好客 · 买美酒,享好礼,全城门店可用'; -export const WECHAT_SHARE_HINT = '请点击右上角 ··· 分享给好友'; +export type ShareScene = + | 'home' + | 'stores' + | 'storeDetail' + | 'benefit' + | 'mine' + | 'productDetail' + | 'orderDetail'; -export function getDefaultShareImageUrl(): string { - return getBrandAssetsSync().brandLogoUrl; +export { DEFAULT_SHARE_TITLE, DEFAULT_SHARE_DESC }; + +export const WECHAT_SHARE_HINT = DEFAULT_SHARE_HINT; + +const FALLBACK_SHARE: MiniShareRuntime = resolveMiniShareRuntime({}); + +let shareCached: MiniShareRuntime | null = null; + +export function getShareRuntimeSync(): MiniShareRuntime { + return shareCached ?? FALLBACK_SHARE; } -/** 预热分享默认图(来自系统设置) */ +export function applyShareFromClientConfig(config?: ClientRuntimeConfig | null) { + if (config?.share) { + shareCached = config.share; + return shareCached; + } + return getShareRuntimeSync(); +} + +export async function loadShareConfig(force = false): Promise { + if (!force && shareCached) return shareCached; + try { + const cfg = await fetchClientConfig(); + if (cfg?.share) { + shareCached = cfg.share; + return shareCached; + } + } catch { + /* ignore */ + } + shareCached = FALLBACK_SHARE; + return shareCached; +} + +export function getDefaultShareImageUrl(): string { + const share = getShareRuntimeSync(); + return share.default.imageUrl || getBrandAssetsSync().brandLogoUrl; +} + +export function getShareHint(): string { + return getShareRuntimeSync().hint || DEFAULT_SHARE_HINT; +} + +/** 预热分享配置与品牌图 */ export function prefetchShareBrandAssets() { void loadBrandAssets(); + void loadShareConfig(); +} + +function sceneConfig(scene?: ShareScene): MiniShareSceneConfig { + const runtime = getShareRuntimeSync(); + if (!scene) return runtime.default; + return runtime[scene] ?? runtime.default; +} + +/** + * 组装页面分享:场景配置优先;空字段用 dynamic → 默认分享。 + * orderDetail 标题支持 {productName}。 + */ +export function buildSceneSharePayload( + scene: ShareScene, + options?: { + path?: string; + /** 业务动态标题(场景配置为空时使用) */ + dynamicTitle?: string | null; + dynamicDesc?: string | null; + dynamicImageUrl?: string | null; + titleVars?: Record; + }, +): PageSharePayload { + const def = getShareRuntimeSync().default; + const sc = sceneConfig(scene); + let title = (sc.title || '').trim(); + if (title && options?.titleVars) { + const vars = options.titleVars; + const missingRequired = Object.entries(vars).some( + ([key, value]) => title.includes(`{${key}}`) && !(value != null && String(value).trim()), + ); + title = missingRequired ? '' : applyShareTitleTemplate(title, vars); + } + if (!title) { + title = (options?.dynamicTitle || '').trim() || def.title; + } + const desc = (sc.desc || '').trim() || (options?.dynamicDesc || '').trim() || def.desc; + const imgUrl = + (sc.imageUrl || '').trim() || + (options?.dynamicImageUrl || '').trim() || + def.imageUrl || + getDefaultShareImageUrl(); + return { + title, + desc, + path: options?.path, + imgUrl, + }; } export function buildDefaultShareData( @@ -29,9 +134,10 @@ export function buildDefaultShareData( link = ''; } } + const def = getShareRuntimeSync().default; return { - title: overrides?.title ?? DEFAULT_SHARE_TITLE, - desc: overrides?.desc ?? DEFAULT_SHARE_DESC, + title: overrides?.title ?? def.title, + desc: overrides?.desc ?? def.desc, link, imgUrl: overrides?.imgUrl ?? getDefaultShareImageUrl(), }; @@ -73,7 +179,7 @@ export async function handleShareButtonClick( /* ignore */ } } - toast(WECHAT_SHARE_HINT); + toast(getShareHint()); return { showGuide: false }; } @@ -94,7 +200,7 @@ export async function handleShareButtonClick( if (result.invoked) { return { showGuide: false }; } - toast(WECHAT_SHARE_HINT); + toast(getShareHint()); return { showGuide: true }; } catch (e) { toast(e instanceof Error ? e.message : '分享配置失败,请刷新后重试'); @@ -104,9 +210,20 @@ export async function handleShareButtonClick( /** 供 useShareAppMessage 使用的标题/路径/图 */ export function toWeappShareMessage(payload?: PageSharePayload) { + const def = getShareRuntimeSync().default; return { - title: payload?.title || DEFAULT_SHARE_TITLE, + title: payload?.title || def.title, path: payload?.path || '/pages/home/index', imageUrl: payload?.imgUrl || getDefaultShareImageUrl(), }; } + +/** 供 useShareTimeline */ +export function toWeappShareTimeline(payload?: PageSharePayload, query = '') { + const def = getShareRuntimeSync().default; + return { + title: payload?.title || def.title, + query, + imageUrl: payload?.imgUrl || getDefaultShareImageUrl(), + }; +} diff --git a/apps/mini-user/src/pages/benefit/index.tsx b/apps/mini-user/src/pages/benefit/index.tsx index 83c4108..3debfe9 100644 --- a/apps/mini-user/src/pages/benefit/index.tsx +++ b/apps/mini-user/src/pages/benefit/index.tsx @@ -8,9 +8,9 @@ import { goLogin } from '../../lib/auth-nav'; import { isLoggedIn, request, toast } from '../../lib/api'; import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../../lib/nav-bar'; import { - DEFAULT_SHARE_DESC, - DEFAULT_SHARE_TITLE, + buildSceneSharePayload, toWeappShareMessage, + toWeappShareTimeline, } from '../../lib/wechat-share'; import { formatMoney } from '../../lib/money'; import iconBenefit from '../../assets/tabbar/benefit-active.png'; @@ -104,20 +104,15 @@ export default function BenefitPage() { const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0); const sharePayload = useMemo( - () => ({ - title: '好客权益 · 杜康好客', - desc: DEFAULT_SHARE_DESC, - path: '/pages/benefit/index', - }), + () => + buildSceneSharePayload('benefit', { + path: '/pages/benefit/index', + }), [], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); - useShareTimeline(() => ({ - title: sharePayload.title || DEFAULT_SHARE_TITLE, - query: '', - imageUrl: sharePayload.imgUrl, - })); + useShareTimeline(() => toWeappShareTimeline(sharePayload)); return ( diff --git a/apps/mini-user/src/pages/home/index.tsx b/apps/mini-user/src/pages/home/index.tsx index 0ddfdee..9b7090a 100644 --- a/apps/mini-user/src/pages/home/index.tsx +++ b/apps/mini-user/src/pages/home/index.tsx @@ -28,10 +28,12 @@ import { normalizeFulfillmentFlags, } from '../../lib/product-fulfillment'; import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location'; +import type { ClientRuntimeConfig } from '@dukang/shared-types'; import { - DEFAULT_SHARE_DESC, - DEFAULT_SHARE_TITLE, + applyShareFromClientConfig, + buildSceneSharePayload, toWeappShareMessage, + toWeappShareTimeline, } from '../../lib/wechat-share'; import { trackPageView } from '../../lib/analytics'; type Product = { @@ -85,8 +87,9 @@ export default function HomePage() { }, [cityCode]); const loadMiniHome = useCallback(() => { - return request<{ miniHome?: MiniHomeConfig }>('/common/client-config') + return request('/common/client-config') .then((cfg) => { + applyShareFromClientConfig(cfg); const banners = Array.isArray(cfg.miniHome?.banners) ? cfg.miniHome!.banners.filter((u) => typeof u === 'string' && !!u.trim()) : []; @@ -221,21 +224,16 @@ export default function HomePage() { const footerUrl = miniHome.footerUrl; const sharePayload = useMemo( - () => ({ - title: DEFAULT_SHARE_TITLE, - desc: DEFAULT_SHARE_DESC, - path: '/pages/home/index', - imgUrl: banners[0] || undefined, - }), + () => + buildSceneSharePayload('home', { + path: '/pages/home/index', + dynamicImageUrl: banners[0] || undefined, + }), [banners], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); - useShareTimeline(() => ({ - title: sharePayload.title || DEFAULT_SHARE_TITLE, - query: '', - imageUrl: sharePayload.imgUrl, - })); + useShareTimeline(() => toWeappShareTimeline(sharePayload)); function scrollToAroma(key: AromaKey) { setActiveAroma(key); diff --git a/apps/mini-user/src/pages/mine/index.tsx b/apps/mini-user/src/pages/mine/index.tsx index fcedce3..8f5c0fa 100644 --- a/apps/mini-user/src/pages/mine/index.tsx +++ b/apps/mini-user/src/pages/mine/index.tsx @@ -29,9 +29,10 @@ import { isWechatEnv } from '../../lib/weixin'; import { APP_VERSION_LABEL } from '../../lib/client-version'; import { maskPhone } from '../../lib/phone'; import { - DEFAULT_SHARE_DESC, - DEFAULT_SHARE_TITLE, + applyShareFromClientConfig, + buildSceneSharePayload, toWeappShareMessage, + toWeappShareTimeline, } from '../../lib/wechat-share'; import iconPendingPay from '../../assets/icons/待付款.png'; import iconPaid from '../../assets/icons/已付款.png'; @@ -77,7 +78,7 @@ export default function MinePage() { const [qualificationUrl, setQualificationUrl] = useState( () => getBrandAssetsSync().qualificationDisclosureUrl, ); - + const [shareTick, setShareTick] = useState(0); function resetGuestState() { setProfile(null); setBenefitBalance(0); @@ -151,8 +152,10 @@ export default function MinePage() { .then((config) => { setWxAuthorize(isWxAuthorizeEnabled(config)); const brand = applyBrandFromClientConfig(config); + applyShareFromClientConfig(config); setBrandMarkUrl(brand.brandLogoMarkUrl); setQualificationUrl(brand.qualificationDisclosureUrl); + setShareTick((n) => n + 1); }) .catch(() => { setWxAuthorize(true); @@ -161,19 +164,15 @@ export default function MinePage() { }, []); const sharePayload = useMemo( - () => ({ - title: '杜康好客 · 我的', - desc: DEFAULT_SHARE_DESC, - path: '/pages/mine/index', - }), - [], + () => + buildSceneSharePayload('mine', { + path: '/pages/mine/index', + }), + [shareTick], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); - useShareTimeline(() => ({ - title: sharePayload.title || DEFAULT_SHARE_TITLE, - query: '', - })); + useShareTimeline(() => toWeappShareTimeline(sharePayload)); async function ensureWechatBound(): Promise { if (profile?.hasWechat) return true; diff --git a/apps/mini-user/src/pages/order-detail/index.tsx b/apps/mini-user/src/pages/order-detail/index.tsx index 25c6c22..be227d2 100644 --- a/apps/mini-user/src/pages/order-detail/index.tsx +++ b/apps/mini-user/src/pages/order-detail/index.tsx @@ -22,9 +22,9 @@ import { fetchOrderTrack } from '../../lib/order-logistics'; import { maskPhone } from '../../lib/phone'; import { confirmOrderReceive, type WechatConfirmPayload } from '../../lib/wechat-order-confirm'; import { - DEFAULT_SHARE_DESC, - DEFAULT_SHARE_TITLE, + buildSceneSharePayload, toWeappShareMessage, + toWeappShareTimeline, } from '../../lib/wechat-share'; import { usePageView } from '../../lib/usePageView'; @@ -174,19 +174,20 @@ export default function OrderDetailPage() { : ''; const sharePayload = useMemo( - () => ({ - title: productName !== '杜康商品' ? `我买了${productName} · 杜康好客` : DEFAULT_SHARE_TITLE, - desc: DEFAULT_SHARE_DESC, - path: orderId ? `/pages/order-detail/index?id=${orderId}` : '/pages/home/index', - }), + () => + buildSceneSharePayload('orderDetail', { + path: orderId ? `/pages/order-detail/index?id=${orderId}` : '/pages/home/index', + titleVars: { + productName: productName && productName !== '杜康商品' ? productName : '', + }, + }), [productName, orderId], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); - useShareTimeline(() => ({ - title: sharePayload.title || DEFAULT_SHARE_TITLE, - query: orderId ? `id=${orderId}` : '', - })); + useShareTimeline(() => + toWeappShareTimeline(sharePayload, orderId ? `id=${orderId}` : ''), + ); function goPay() { if (!order) return; diff --git a/apps/mini-user/src/pages/product-detail/index.tsx b/apps/mini-user/src/pages/product-detail/index.tsx index d3b72a8..b083fcd 100644 --- a/apps/mini-user/src/pages/product-detail/index.tsx +++ b/apps/mini-user/src/pages/product-detail/index.tsx @@ -27,9 +27,9 @@ import { normalizeFulfillmentFlags, } from '../../lib/product-fulfillment'; import { - DEFAULT_SHARE_DESC, - DEFAULT_SHARE_TITLE, + buildSceneSharePayload, toWeappShareMessage, + toWeappShareTimeline, } from '../../lib/wechat-share'; import iconHome from '../../assets/tabbar/home.png'; import { usePageView } from '../../lib/usePageView'; @@ -88,21 +88,20 @@ export default function ProductDetailPage() { }); const sharePayload = useMemo( - () => ({ - title: product?.name || DEFAULT_SHARE_TITLE, - desc: product?.subtitle || DEFAULT_SHARE_DESC, - path: `/pages/product-detail/index?id=${productId}`, - imgUrl: (product ? getProductMainImage(product) : '') || undefined, - }), + () => + buildSceneSharePayload('productDetail', { + path: `/pages/product-detail/index?id=${productId}`, + dynamicTitle: product?.name, + dynamicDesc: product?.subtitle, + dynamicImageUrl: product ? getProductMainImage(product) : undefined, + }), [product, productId], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); - useShareTimeline(() => ({ - title: sharePayload.title || DEFAULT_SHARE_TITLE, - query: productId ? `id=${productId}` : '', - imageUrl: sharePayload.imgUrl, - })); + useShareTimeline(() => + toWeappShareTimeline(sharePayload, productId ? `id=${productId}` : ''), + ); function goBack() { const pages = Taro.getCurrentPages(); diff --git a/apps/mini-user/src/pages/store-detail/index.tsx b/apps/mini-user/src/pages/store-detail/index.tsx index f28ad37..a966b67 100644 --- a/apps/mini-user/src/pages/store-detail/index.tsx +++ b/apps/mini-user/src/pages/store-detail/index.tsx @@ -19,9 +19,9 @@ import { maskPhone } from '../../lib/phone'; import { track } from '../../lib/analytics'; import { formatShanghaiDateTime } from '../../lib/datetime'; import { - DEFAULT_SHARE_DESC, - DEFAULT_SHARE_TITLE, + buildSceneSharePayload, toWeappShareMessage, + toWeappShareTimeline, } from '../../lib/wechat-share'; type StoreMedia = { @@ -237,18 +237,15 @@ export default function StoreDetailPage() { void loadRecentRedeems(id); }); - const sharePayload = useMemo( - () => { - const envFirst = store ? envPhotoUrls(store)[0] : undefined; - return { - title: store?.name || DEFAULT_SHARE_TITLE, - desc: store?.intro?.trim() || store?.address || DEFAULT_SHARE_DESC, - path: `/pages/store-detail/index?id=${storeId}`, - imgUrl: store?.coverUrl || envFirst || store?.carouselUrls?.[0] || undefined, - }; - }, - [store, storeId], - ); + const sharePayload = useMemo(() => { + const envFirst = store ? envPhotoUrls(store)[0] : undefined; + return buildSceneSharePayload('storeDetail', { + path: `/pages/store-detail/index?id=${storeId}`, + dynamicTitle: store?.name, + dynamicDesc: store?.intro?.trim() || store?.address, + dynamicImageUrl: store?.coverUrl || envFirst || store?.carouselUrls?.[0], + }); + }, [store, storeId]); const marqueeLines = useMemo( () => recentRedeems.map(formatRecentRedeemLine).filter(Boolean), @@ -256,11 +253,9 @@ export default function StoreDetailPage() { ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); - useShareTimeline(() => ({ - title: sharePayload.title || DEFAULT_SHARE_TITLE, - query: storeId ? `id=${storeId}` : '', - imageUrl: sharePayload.imgUrl, - })); + useShareTimeline(() => + toWeappShareTimeline(sharePayload, storeId ? `id=${storeId}` : ''), + ); function goBack() { const pages = Taro.getCurrentPages(); diff --git a/apps/mini-user/src/pages/stores/index.tsx b/apps/mini-user/src/pages/stores/index.tsx index f33e287..5a52db0 100644 --- a/apps/mini-user/src/pages/stores/index.tsx +++ b/apps/mini-user/src/pages/stores/index.tsx @@ -36,9 +36,9 @@ import { setStoresListCache, } from '../../lib/stores-session'; import { - DEFAULT_SHARE_DESC, - DEFAULT_SHARE_TITLE, + buildSceneSharePayload, toWeappShareMessage, + toWeappShareTimeline, } from '../../lib/wechat-share'; type Store = { @@ -350,19 +350,15 @@ export default function StoresPage() { } const sharePayload = useMemo( - () => ({ - title: '杜康好客门店', - desc: DEFAULT_SHARE_DESC, - path: '/pages/stores/index', - }), + () => + buildSceneSharePayload('stores', { + path: '/pages/stores/index', + }), [], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); - useShareTimeline(() => ({ - title: sharePayload.title || DEFAULT_SHARE_TITLE, - query: '', - })); + useShareTimeline(() => toWeappShareTimeline(sharePayload)); return ( diff --git a/packages/shared-types/src/config.ts b/packages/shared-types/src/config.ts index 17887a9..0716b3c 100644 --- a/packages/shared-types/src/config.ts +++ b/packages/shared-types/src/config.ts @@ -104,6 +104,97 @@ export function resolveMockSmsFixedCode(env?: Record return code || MOCK_SMS_FIXED_CODE; } +/** 小程序 / H5 默认分享文案与引导(系统设置「小程序分享配置」可覆盖) */ +export const DEFAULT_SHARE_TITLE = '你吃饭,我买单'; +export const DEFAULT_SHARE_DESC = '杜康好客 · 买美酒,享好礼,全城门店可用'; +export const DEFAULT_SHARE_HINT = '请点击右上角 ··· 分享给好友'; +export const DEFAULT_SHARE_STORES_TITLE = '杜康好客门店'; +export const DEFAULT_SHARE_BENEFIT_TITLE = '好客权益 · 杜康好客'; +export const DEFAULT_SHARE_MINE_TITLE = '杜康好客 · 我的'; +/** 订单详情分享标题模板,可用 {productName} */ +export const DEFAULT_SHARE_ORDER_TITLE = '我买了{productName} · 杜康好客'; + +export type MiniShareSceneConfig = { + title: string; + desc: string; + imageUrl: string; +}; + +/** 各场景分享文案/图(空字符串表示该字段走动态内容或默认分享) */ +export type MiniShareRuntime = { + hint: string; + default: MiniShareSceneConfig; + home: MiniShareSceneConfig; + stores: MiniShareSceneConfig; + storeDetail: MiniShareSceneConfig; + benefit: MiniShareSceneConfig; + mine: MiniShareSceneConfig; + productDetail: MiniShareSceneConfig; + orderDetail: MiniShareSceneConfig; +}; + +function pickShareScene( + e: Record, + prefix: string, + fallback: Partial = {}, +): MiniShareSceneConfig { + return { + title: (e[`${prefix}_TITLE`] || '').trim() || fallback.title || '', + desc: (e[`${prefix}_DESC`] || '').trim() || fallback.desc || '', + imageUrl: (e[`${prefix}_IMAGE_URL`] || '').trim() || fallback.imageUrl || '', + }; +} + +/** 从 env / 系统设置解析小程序分享配置 */ +export function resolveMiniShareRuntime( + env?: Record, +): MiniShareRuntime { + const e = readEnv(env); + const brand = resolveClientBrandRuntime(e); + const defaults: MiniShareSceneConfig = { + title: (e.SHARE_DEFAULT_TITLE || '').trim() || DEFAULT_SHARE_TITLE, + desc: (e.SHARE_DEFAULT_DESC || '').trim() || DEFAULT_SHARE_DESC, + imageUrl: (e.SHARE_DEFAULT_IMAGE_URL || '').trim() || brand.brandLogoUrl, + }; + return { + hint: (e.SHARE_HINT || '').trim() || DEFAULT_SHARE_HINT, + default: defaults, + home: pickShareScene(e, 'SHARE_HOME', { + title: defaults.title, + desc: defaults.desc, + }), + stores: pickShareScene(e, 'SHARE_STORES', { + title: DEFAULT_SHARE_STORES_TITLE, + desc: defaults.desc, + }), + storeDetail: pickShareScene(e, 'SHARE_STORE_DETAIL'), + benefit: pickShareScene(e, 'SHARE_BENEFIT', { + title: DEFAULT_SHARE_BENEFIT_TITLE, + desc: defaults.desc, + }), + mine: pickShareScene(e, 'SHARE_MINE', { + title: DEFAULT_SHARE_MINE_TITLE, + desc: defaults.desc, + }), + productDetail: pickShareScene(e, 'SHARE_PRODUCT_DETAIL'), + orderDetail: pickShareScene(e, 'SHARE_ORDER_DETAIL', { + title: DEFAULT_SHARE_ORDER_TITLE, + desc: defaults.desc, + }), + }; +} + +/** 订单等标题模板:替换 {productName} 等占位符 */ +export function applyShareTitleTemplate( + template: string, + vars: Record, +): string { + return template.replace(/\{(\w+)\}/g, (_, key: string) => { + const v = vars[key]; + return v != null && String(v).trim() ? String(v).trim() : ''; + }).replace(/\s{2,}/g, ' ').trim(); +} + function readEnv(env?: Record) { return ( env ?? diff --git a/packages/shared-types/src/wechat.ts b/packages/shared-types/src/wechat.ts index d5709c6..886acff 100644 --- a/packages/shared-types/src/wechat.ts +++ b/packages/shared-types/src/wechat.ts @@ -1,3 +1,5 @@ +import type { MiniShareRuntime } from './config'; + /** 微信 JSSDK 初始化参数(后端签名下发) */ export interface WechatJssdkConfig { appId: string; @@ -62,6 +64,8 @@ export type ClientRuntimeConfig = { qualificationDisclosureUrl?: string; /** 总部客服电话 */ customerServicePhone?: string; + /** 小程序各场景分享文案/图 */ + share?: MiniShareRuntime; }; /** 是否展示微信授权入口 */ diff --git a/server/dukang-api/src/common/system-config/system-config.registry.ts b/server/dukang-api/src/common/system-config/system-config.registry.ts index 81bc53d..11f2ad9 100644 --- a/server/dukang-api/src/common/system-config/system-config.registry.ts +++ b/server/dukang-api/src/common/system-config/system-config.registry.ts @@ -5,6 +5,13 @@ import { BRAND_LOGO_URL, BRAND_LOGO_WIDE_URL, CUSTOMER_SERVICE_PHONE, + DEFAULT_SHARE_BENEFIT_TITLE, + DEFAULT_SHARE_DESC, + DEFAULT_SHARE_HINT, + DEFAULT_SHARE_MINE_TITLE, + DEFAULT_SHARE_ORDER_TITLE, + DEFAULT_SHARE_STORES_TITLE, + DEFAULT_SHARE_TITLE, DEFAULT_USER_H5_URL, MINI_USER_STATIC_OSS_BASE, MOCK_SMS_FIXED_CODE, @@ -17,6 +24,7 @@ export const SYSTEM_CONFIG_GROUPS: SystemConfigGroupMeta[] = [ { key: 'sms', label: '短信' }, { key: 'wechat', label: '微信' }, { key: 'wechat_mini', label: '微信小程序配置' }, + { key: 'wechat_mini_share', label: '小程序分享配置' }, { key: 'oss', label: '对象存储 OSS' }, { key: 'app', label: '应用链接' }, { key: 'deploy', label: '发布部署' }, @@ -29,6 +37,7 @@ const G = { sms: 'sms', wechat: 'wechat', wechat_mini: 'wechat_mini', + wechat_mini_share: 'wechat_mini_share', oss: 'oss', app: 'app', deploy: 'deploy', @@ -160,7 +169,7 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [ group: G.wechat_mini, type: 'image', requiresRestart: false, - description: '分享卡片默认图、首页等品牌展示', + description: '品牌方形 Logo;分享默认图未单独配置时回退此图', }, { key: 'BRAND_LOGO_WIDE_URL', @@ -214,6 +223,202 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [ description: '仅 MOCK_SMS 开启时生效;留空则用默认 999888', }, + // —— 小程序分享配置 —— + { + key: 'SHARE_HINT', + label: '分享引导文案', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + placeholder: DEFAULT_SHARE_HINT, + description: '点击分享按钮后的 Toast 提示(小程序内)', + }, + { + key: 'SHARE_DEFAULT_TITLE', + label: '默认分享标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + placeholder: DEFAULT_SHARE_TITLE, + description: '各场景未单独配置标题时使用', + }, + { + key: 'SHARE_DEFAULT_DESC', + label: '默认分享描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + placeholder: DEFAULT_SHARE_DESC, + description: 'H5 分享卡片描述;小程序朋友圈/好友卡片副文案回退', + }, + { + key: 'SHARE_DEFAULT_IMAGE_URL', + label: '默认分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + description: '各场景未配置图且无业务图时使用;建议接近 5:4', + }, + { + key: 'SHARE_HOME_TITLE', + label: '首页 · 标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + }, + { + key: 'SHARE_HOME_DESC', + label: '首页 · 描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + }, + { + key: 'SHARE_HOME_IMAGE_URL', + label: '首页 · 分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + description: '留空则优先用首页首张轮播图,再回退默认分享图', + }, + { + key: 'SHARE_STORES_TITLE', + label: '门店列表 · 标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + placeholder: DEFAULT_SHARE_STORES_TITLE, + }, + { + key: 'SHARE_STORES_DESC', + label: '门店列表 · 描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + }, + { + key: 'SHARE_STORES_IMAGE_URL', + label: '门店列表 · 分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + }, + { + key: 'SHARE_STORE_DETAIL_TITLE', + label: '门店详情 · 标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + description: '留空则用门店名称', + }, + { + key: 'SHARE_STORE_DETAIL_DESC', + label: '门店详情 · 描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + description: '留空则用门店简介/地址', + }, + { + key: 'SHARE_STORE_DETAIL_IMAGE_URL', + label: '门店详情 · 分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + description: '留空则用门头图/环境图', + }, + { + key: 'SHARE_BENEFIT_TITLE', + label: '权益页 · 标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + placeholder: DEFAULT_SHARE_BENEFIT_TITLE, + }, + { + key: 'SHARE_BENEFIT_DESC', + label: '权益页 · 描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + }, + { + key: 'SHARE_BENEFIT_IMAGE_URL', + label: '权益页 · 分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + }, + { + key: 'SHARE_MINE_TITLE', + label: '我的 · 标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + placeholder: DEFAULT_SHARE_MINE_TITLE, + }, + { + key: 'SHARE_MINE_DESC', + label: '我的 · 描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + }, + { + key: 'SHARE_MINE_IMAGE_URL', + label: '我的 · 分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + }, + { + key: 'SHARE_PRODUCT_DETAIL_TITLE', + label: '商品详情 · 标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + description: '留空则用商品名称', + }, + { + key: 'SHARE_PRODUCT_DETAIL_DESC', + label: '商品详情 · 描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + description: '留空则用商品副标题', + }, + { + key: 'SHARE_PRODUCT_DETAIL_IMAGE_URL', + label: '商品详情 · 分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + description: '留空则用商品主图', + }, + { + key: 'SHARE_ORDER_DETAIL_TITLE', + label: '订单详情 · 标题', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + placeholder: DEFAULT_SHARE_ORDER_TITLE, + description: '支持 {productName} 占位符', + }, + { + key: 'SHARE_ORDER_DETAIL_DESC', + label: '订单详情 · 描述', + group: G.wechat_mini_share, + type: 'string', + requiresRestart: false, + }, + { + key: 'SHARE_ORDER_DETAIL_IMAGE_URL', + label: '订单详情 · 分享图', + group: G.wechat_mini_share, + type: 'image', + requiresRestart: false, + }, + { key: 'OSS_ACCESS_KEY_ID', label: 'OSS AccessKey ID', group: G.oss, type: 'password', secret: true, requiresRestart: true }, { key: 'OSS_ACCESS_KEY_SECRET', label: 'OSS AccessKey Secret', group: G.oss, type: 'password', secret: true, requiresRestart: true }, { key: 'OSS_BUCKET', label: 'OSS Bucket', group: G.oss, type: 'string', requiresRestart: true }, @@ -345,6 +550,20 @@ export const SYSTEM_CONFIG_DEFAULTS: Record = { QUALIFICATION_DISCLOSURE_URL: QUALIFICATION_DISCLOSURE_URL, CUSTOMER_SERVICE_PHONE: CUSTOMER_SERVICE_PHONE, MOCK_SMS_FIXED_CODE: MOCK_SMS_FIXED_CODE, + SHARE_HINT: DEFAULT_SHARE_HINT, + SHARE_DEFAULT_TITLE: DEFAULT_SHARE_TITLE, + SHARE_DEFAULT_DESC: DEFAULT_SHARE_DESC, + SHARE_DEFAULT_IMAGE_URL: BRAND_LOGO_URL, + SHARE_HOME_TITLE: DEFAULT_SHARE_TITLE, + SHARE_HOME_DESC: DEFAULT_SHARE_DESC, + SHARE_STORES_TITLE: DEFAULT_SHARE_STORES_TITLE, + SHARE_STORES_DESC: DEFAULT_SHARE_DESC, + SHARE_BENEFIT_TITLE: DEFAULT_SHARE_BENEFIT_TITLE, + SHARE_BENEFIT_DESC: DEFAULT_SHARE_DESC, + SHARE_MINE_TITLE: DEFAULT_SHARE_MINE_TITLE, + SHARE_MINE_DESC: DEFAULT_SHARE_DESC, + SHARE_ORDER_DETAIL_TITLE: DEFAULT_SHARE_ORDER_TITLE, + SHARE_ORDER_DETAIL_DESC: DEFAULT_SHARE_DESC, }; export function getSystemConfigField(key: string): SystemConfigFieldMeta | undefined { diff --git a/server/dukang-api/src/modules/common/client-config.controller.ts b/server/dukang-api/src/modules/common/client-config.controller.ts index 9716bd5..84c56b5 100644 --- a/server/dukang-api/src/modules/common/client-config.controller.ts +++ b/server/dukang-api/src/modules/common/client-config.controller.ts @@ -1,5 +1,9 @@ import { Controller, Get } from '@nestjs/common'; -import { parseMiniHomeBanners, resolveClientBrandRuntime } from '@dukang/shared-types'; +import { + parseMiniHomeBanners, + resolveClientBrandRuntime, + resolveMiniShareRuntime, +} from '@dukang/shared-types'; import { SystemConfigService } from '../../common/system-config/system-config.service'; @Controller('common') @@ -13,6 +17,7 @@ export class ClientConfigController { const footer = (env.MINI_HOME_FOOTER_URL ?? '').trim(); const minClientVersion = (env.MINI_USER_MIN_VERSION ?? '').trim() || null; const brand = resolveClientBrandRuntime(env); + const share = resolveMiniShareRuntime(env); return { mockPay: cfg.mockPay, wechatPayEnabled: cfg.wechatPayEnabled, @@ -32,6 +37,7 @@ export class ClientConfigController { brandLogoMarkUrl: brand.brandLogoMarkUrl, qualificationDisclosureUrl: brand.qualificationDisclosureUrl, customerServicePhone: brand.customerServicePhone, + share, }; } }