diff --git a/apps/admin-web/package.json b/apps/admin-web/package.json index a911b70..b784763 100644 --- a/apps/admin-web/package.json +++ b/apps/admin-web/package.json @@ -1,6 +1,6 @@ { "name": "@dukang/admin-web", - "version": "4.0.18", + "version": "4.0.19", "private": true, "type": "module", "scripts": { diff --git a/apps/h5-partner/package.json b/apps/h5-partner/package.json index 0419e39..4e25d06 100644 --- a/apps/h5-partner/package.json +++ b/apps/h5-partner/package.json @@ -1,6 +1,6 @@ { "name": "@dukang/h5-partner", - "version": "4.0.18", + "version": "4.0.19", "private": true, "type": "module", "scripts": { diff --git a/apps/h5-shop/package.json b/apps/h5-shop/package.json index 8c57d8a..11b91c8 100644 --- a/apps/h5-shop/package.json +++ b/apps/h5-shop/package.json @@ -1,6 +1,6 @@ { "name": "@dukang/h5-shop", - "version": "4.0.18", + "version": "4.0.19", "private": true, "type": "module", "scripts": { diff --git a/apps/mini-user/package.json b/apps/mini-user/package.json index 1876457..8dc6d45 100644 --- a/apps/mini-user/package.json +++ b/apps/mini-user/package.json @@ -1,6 +1,6 @@ { "name": "@dukang/mini-user", - "version": "4.0.18", + "version": "4.0.19", "private": true, "description": "杜康好客 · C 端用户微信小程序(Taro)", "scripts": { diff --git a/apps/mini-user/src/lib/checkout-nav.ts b/apps/mini-user/src/lib/checkout-nav.ts index 171481c..739c1a7 100644 --- a/apps/mini-user/src/lib/checkout-nav.ts +++ b/apps/mini-user/src/lib/checkout-nav.ts @@ -52,6 +52,19 @@ export function buildPayUrl(params: { return `/pages/pay/index?${parts.join('&')}`; } +export function buildOrderConfirmPickupUrl(ctx: { + productId: string; + skuId?: string; + qty?: string | number; +}): string { + const parts = [`productId=${encodeURIComponent(ctx.productId)}`]; + if (ctx.skuId) parts.push(`skuId=${encodeURIComponent(ctx.skuId)}`); + if (ctx.qty != null && String(ctx.qty).trim()) { + parts.push(`qty=${encodeURIComponent(String(ctx.qty))}`); + } + return `/pages/order-confirm-pickup/index?${parts.join('&')}`; +} + export function readCheckoutContext(params: Record): CheckoutContext { return { productId: params.productId, diff --git a/apps/mini-user/src/lib/client-version.ts b/apps/mini-user/src/lib/client-version.ts index 5b441aa..ab276f6 100644 --- a/apps/mini-user/src/lib/client-version.ts +++ b/apps/mini-user/src/lib/client-version.ts @@ -3,7 +3,7 @@ import { fetchClientConfig } from './pay-wechat'; /** 与 package.json version 同步(Taro defineConstants 注入),供 minClientVersion 比对 */ export const APP_VERSION = - (typeof TARO_APP_VERSION !== 'undefined' && String(TARO_APP_VERSION).trim()) || '4.0.4'; + (typeof TARO_APP_VERSION !== 'undefined' && String(TARO_APP_VERSION).trim()) || '4.0.19'; export const APP_VERSION_LABEL = `v${APP_VERSION.replace(/^v/i, '')}`; diff --git a/apps/mini-user/src/pages/order-confirm-pickup/index.tsx b/apps/mini-user/src/pages/order-confirm-pickup/index.tsx index bcad216..272a58e 100644 --- a/apps/mini-user/src/pages/order-confirm-pickup/index.tsx +++ b/apps/mini-user/src/pages/order-confirm-pickup/index.tsx @@ -5,9 +5,10 @@ import Taro, { useRouter } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; import { goLogin } from '../../lib/auth-nav'; -import { buildPayUrl } from '../../lib/checkout-nav'; +import { buildOrderConfirmUrl, buildPayUrl } from '../../lib/checkout-nav'; import { fetchUserProfile } from '../../lib/pay-wechat'; import { request, toast } from '../../lib/api'; +import { canBuyOnline } from '../../lib/product-fulfillment'; import { getProductMainImage } from '../../lib/product-images'; import BenefitFigure from '../../components/BenefitFigure'; import OrderQtyControls from '../../components/OrderQtyControls'; @@ -20,6 +21,7 @@ type PreviewProduct = { price: number; mainImageUrl?: string | null; carouselUrls?: string[] | null; + allowOnlinePurchase?: boolean; }; type OrderPreview = { @@ -174,13 +176,35 @@ export default function OrderConfirmPickupPage() { : !quantityOk ? `至少购买 ${minQty}${unitLabel}` : '提交订单'; + const allowOnline = canBuyOnline(preview?.product ?? {}); + + function goDelivery() { + if (!productId || !allowOnline) return; + Taro.redirectTo({ + url: buildOrderConfirmUrl({ + productId, + skuId: skuId || undefined, + qty: String(quantity), + }), + }); + } return ( - 取货方式 + 收货方式 + {allowOnline ? ( + + + 配送到家 + + + 现场提货 + + + ) : null} 现场取货 · 无需填写收货地址 · 免运费 diff --git a/apps/mini-user/src/pages/order-confirm/index.tsx b/apps/mini-user/src/pages/order-confirm/index.tsx index 2281c09..5ac83c2 100644 --- a/apps/mini-user/src/pages/order-confirm/index.tsx +++ b/apps/mini-user/src/pages/order-confirm/index.tsx @@ -5,13 +5,13 @@ import Taro, { useRouter } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; import { goLogin } from '../../lib/auth-nav'; -import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav'; +import { buildAddressListUrl, buildOrderConfirmPickupUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav'; import { tryGetClientGpsLocation } from '../../lib/client-location'; import { maskPhone } from '../../lib/phone'; import { fetchUserProfile } from '../../lib/pay-wechat'; import { request, toast } from '../../lib/api'; import DeliveryHintHtml from '../../components/DeliveryHintHtml'; -import { canCrossCity, isCrossCityAddress } from '../../lib/product-fulfillment'; +import { canCrossCity, canPickupOnSite, isCrossCityAddress } from '../../lib/product-fulfillment'; import { loadLocalDeliveries, matchLocalDelivery, resolveLocalDeliveryHintHtml } from '../../lib/local-delivery'; import { getProductMainImage } from '../../lib/product-images'; import { isDirtyShippingAddress } from '../../lib/shipping-address'; @@ -39,6 +39,7 @@ type PreviewProduct = { carouselUrls?: string[] | null; allowCrossCityDelivery?: boolean; allowOnlinePurchase?: boolean; + allowOnSitePickup?: boolean; }; type OrderPreview = { @@ -312,36 +313,63 @@ export default function OrderConfirmPage() { ? `至少购买 ${minQty}${unitLabel}` : '提交订单'; const displayMsg = msg || addressHint; + const allowPickup = canPickupOnSite(preview?.product ?? {}); + + function goOnSitePickup() { + if (!productId || !allowPickup) return; + Taro.redirectTo({ + url: buildOrderConfirmPickupUrl({ + productId, + skuId: skuId || undefined, + qty: quantity, + }), + }); + } return ( - - Taro.navigateTo({ - url: buildAddressListUrl({ - productId, - qty: String(quantity), - addressId, - cross: forceCross, - }), - }) - } - > - 收货地址 - {selectedAddress ? ( - - - {selectedAddress.receiverName} - {maskPhone(selectedAddress.phone)} + + 收货方式 + {allowPickup ? ( + + + 配送到家 + + + 现场提货 - {formatAddress(selectedAddress)} - ) : ( - 点击选择收货地址 - )} + ) : null} + {allowPickup ? ( + 人在门店?点「现场提货」,免填地址 + ) : null} + + Taro.navigateTo({ + url: buildAddressListUrl({ + productId, + qty: String(quantity), + addressId, + cross: forceCross, + }), + }) + } + > + 收货地址 + {selectedAddress ? ( + + + {selectedAddress.receiverName} + {maskPhone(selectedAddress.phone)} + + {formatAddress(selectedAddress)} + + ) : ( + 点击选择收货地址 + )} + {!addressOk && addressId ? ( diff --git a/apps/mini-user/src/pages/order-detail/index.tsx b/apps/mini-user/src/pages/order-detail/index.tsx index c80fcbd..a32b68b 100644 --- a/apps/mini-user/src/pages/order-detail/index.tsx +++ b/apps/mini-user/src/pages/order-detail/index.tsx @@ -9,7 +9,7 @@ import WechatShareReady from '../../components/WechatShareReady'; import ContactCsButton from '../../components/ContactCsButton'; import LogisticsRichText from '../../components/LogisticsRichText'; import { request, toast } from '../../lib/api'; -import { buildPayUrl } from '../../lib/checkout-nav'; +import { buildPayUrl, buildOrderConfirmPickupUrl } from '../../lib/checkout-nav'; import DeliveryHintHtml from '../../components/DeliveryHintHtml'; import { loadLocalDeliveries, matchLocalDelivery, resolveLocalDeliveryHintHtml } from '../../lib/local-delivery'; import { @@ -45,6 +45,8 @@ type OrderDetail = { orderNo?: string; status?: string; payAmount?: number; + productId?: string; + skuId?: string | null; productName?: string; quantity?: number; qty?: number; @@ -177,6 +179,11 @@ export default function OrderDetailPage() { const isReship = !!order?.originOrderId; const isProxy = !!order && (order.isProxyOrder || order.orderType === 'PROXY'); const canPay = !!order && order.status === 'PENDING_PAY' && !isReship; + const canSwitchPickup = + canPay && + !isProxy && + order?.deliveryType !== 'ON_SITE_PICKUP' && + !!order?.productId; const canConfirmReceive = !!order && !isReship && !isProxy && ['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || ''); const canInvoice = @@ -226,6 +233,17 @@ export default function OrderDetailPage() { Taro.navigateTo({ url: buildPayUrl({ orderId: order.id }) }); } + function goOnSitePickup() { + if (!order?.productId) return; + Taro.navigateTo({ + url: buildOrderConfirmPickupUrl({ + productId: String(order.productId), + skuId: order.skuId ? String(order.skuId) : undefined, + qty: quantity, + }), + }); + } + function goCustomerService() { Taro.navigateTo({ url: '/pages/customer-service/index' }); } @@ -358,6 +376,22 @@ export default function OrderDetailPage() { 收货信息 + {canSwitchPickup ? ( + + + 配送到家 + + + 现场提货 + + + ) : null} + {canSwitchPickup ? ( + 人在门店?点「现场提货」重新下单,免填地址 + ) : null} {receiverLine || addressText ? ( <> {receiverLine ? ( diff --git a/apps/mini-user/src/styles/order.css b/apps/mini-user/src/styles/order.css index 20d1edb..d6f7b3a 100644 --- a/apps/mini-user/src/styles/order.css +++ b/apps/mini-user/src/styles/order.css @@ -81,6 +81,49 @@ box-shadow: var(--shadow-card); } +.order-fulfillment-switch { + display: flex; + gap: 8px; + margin: 10px 0 12px; +} + +.order-fulfillment-opt { + flex: 1; + height: 40px; + border-radius: 999px; + border: 1px solid var(--color-outline, #c8c4be); + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + font-size: 14px; + font-weight: 700; + color: var(--color-on-surface); +} + +.order-fulfillment-opt--active { + border-color: var(--color-heritage-red); + color: var(--color-heritage-red); + background: rgba(166, 29, 36, 0.08); +} + +.order-fulfillment-opt--pickup { + border-color: var(--color-heritage-red); + color: var(--color-heritage-red); +} + +.order-address-title { + display: block; + font-size: 13px; + margin-bottom: 6px; +} + +.order-pickup-guide { + display: block; + margin-bottom: 10px; + font-size: 12px; +} + .order-card--warn { background: rgba(166, 29, 36, 0.06); box-shadow: none; diff --git a/package.json b/package.json index d2d7890..42b5a37 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "db:sync-benefit": "pnpm --filter @dukang/api prisma:sync-benefit", "db:validate": "pnpm --filter @dukang/api prisma:validate", "oss:cors": "node scripts/configure-oss-cors.mjs", + "set-version": "node scripts/set-version.mjs", "sync:stitch": "node scripts/sync-stitch.mjs", "smoke": "node scripts/smoke-v3.mjs", "smoke:v3": "node scripts/smoke-v3.mjs", diff --git a/packages/shared-types/src/enums.ts b/packages/shared-types/src/enums.ts index 4e6eeb5..137a1cb 100644 --- a/packages/shared-types/src/enums.ts +++ b/packages/shared-types/src/enums.ts @@ -38,6 +38,18 @@ export const USER_SOURCE_TYPE_LABELS: Record = { [UserSourceType.OTHER]: '其他', }; +/** 成交播报等单行展示:类型中文 + 可选来源标签 */ +export function formatUserSourceLine( + sourceType?: string | null, + sourceLabel?: string | null, +): string { + const type = String(sourceType || '').trim(); + const typeLabel = + (type && USER_SOURCE_TYPE_LABELS[type as UserSourceType]) || type || '—'; + const label = String(sourceLabel || '').trim(); + return label ? `${typeLabel} · ${label}` : typeLabel; +} + export enum SmsScene { USER_LOGIN = 'USER_LOGIN', STORE_LOGIN = 'STORE_LOGIN', diff --git a/packages/shared-types/src/wecom-message-push.test.ts b/packages/shared-types/src/wecom-message-push.test.ts index ea192f1..eee099d 100644 --- a/packages/shared-types/src/wecom-message-push.test.ts +++ b/packages/shared-types/src/wecom-message-push.test.ts @@ -7,6 +7,7 @@ import { WECOM_TEMPLATE_PLACEHOLDERS, parseWecomPushConditions, } from './wecom-message-push'; +import { formatUserSourceLine } from './enums'; describe('wecom-message-push', () => { it('alert.settlement 展示为结算任务失败通知且在系统监控组', () => { @@ -35,13 +36,15 @@ describe('wecom-message-push', () => { ]); }); - it('支付与核销模板含昵称、明文手机、备注占位符', () => { + it('支付模板含用户来源占位符', () => { expect(WECOM_TEMPLATE_PLACEHOLDERS['order.paid']).toEqual( - expect.arrayContaining(['userName', 'userPhone', 'userRemark']), + expect.arrayContaining(['userName', 'userPhone', 'userRemark', 'userSource']), ); expect(WECOM_TEMPLATE_PLACEHOLDERS['redeem.success']).toEqual( expect.arrayContaining(['userName', 'userPhone', 'userRemark']), ); + expect(formatUserSourceLine('PROMO_CODE', '秋季品鉴')).toBe('推广码 · 秋季品鉴'); + expect(formatUserSourceLine('ORGANIC', '')).toBe('自然流量'); }); it('结算账单模板含累计金额与明细摘要', () => { diff --git a/packages/shared-types/src/wecom-message-push.ts b/packages/shared-types/src/wecom-message-push.ts index 4b0ed93..402ae7e 100644 --- a/packages/shared-types/src/wecom-message-push.ts +++ b/packages/shared-types/src/wecom-message-push.ts @@ -200,6 +200,7 @@ export const WECOM_TEMPLATE_PLACEHOLDERS: Record [--dry-run] [--include-libs]`); + } else rest.push(a); + } + return { flags, versionRaw: rest[0] }; +} + +function normalizeVersion(raw) { + const v = String(raw || '').trim().replace(/^v/i, ''); + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(v)) { + fail(`版本号须为 semver,例如 4.0.19 或 v4.0.19,收到:${raw || '(空)'}`); + } + return v; +} + +function walkPackageJson(dir, out = []) { + for (const name of readdirSync(dir)) { + if (name === 'node_modules' || name === 'dist' || name === '.git') continue; + const full = join(dir, name); + let st; + try { + st = statSync(full); + } catch { + continue; + } + if (st.isDirectory()) walkPackageJson(full, out); + else if (name === 'package.json') out.push(full); + } + return out; +} + +function rel(file) { + return relative(root, file).replaceAll('\\', '/'); +} + +function printHelp() { + console.log(`统一改产品版本号 + +用法: + node scripts/set-version.mjs [--dry-run] [--include-libs] + pnpm set-version -- + +示例: + node scripts/set-version.mjs 4.0.19 + node scripts/set-version.mjs v4.0.19 --dry-run + +会改: + - 各端 / API / shared-ui 的 package.json version + - 小程序 client-version.ts 兜底 + - API GET /health 的 version + - HQ「小程序最低版本」placeholder + +默认不改内部库 0.1.0(domain / shared-types / weixin-sdk / client-logging)。`); +} + +function main() { + const { flags, versionRaw } = parseArgs(process.argv.slice(2)); + if (flags.has('help') || !versionRaw) { + printHelp(); + process.exit(versionRaw ? 0 : 1); + } + + const version = normalizeVersion(versionRaw); + const dryRun = flags.has('dryRun'); + const includeLibs = flags.has('includeLibs'); + + const changes = []; + + for (const file of walkPackageJson(root)) { + const key = rel(file); + if (key === 'package.json') continue; + if (!includeLibs && LIB_PACKAGES.has(key)) continue; + + const raw = readFileSync(file, 'utf8'); + let pkg; + try { + pkg = JSON.parse(raw); + } catch { + fail(`无法解析 ${key}`); + } + if (typeof pkg.version !== 'string') continue; + if (pkg.version === version) { + changes.push({ file: key, from: pkg.version, to: version, changed: false }); + continue; + } + const next = { ...pkg, version }; + const text = `${JSON.stringify(next, null, 2)}\n`; + if (!dryRun) writeFileSync(file, text); + changes.push({ file: key, from: pkg.version, to: version, changed: true }); + } + + for (const target of SOURCE_TARGETS) { + const file = join(root, target.file); + const raw = readFileSync(file, 'utf8'); + const match = raw.match(target.pattern); + if (!match) { + changes.push({ + file: target.file, + from: '(未匹配到 semver)', + to: version, + changed: false, + note: target.label, + }); + continue; + } + const from = match[2]; + if (from === version) { + changes.push({ file: target.file, from, to: version, changed: false, note: target.label }); + continue; + } + const next = raw.replace(target.pattern, `$1${version}$3`); + if (!dryRun) writeFileSync(file, next); + changes.push({ file: target.file, from, to: version, changed: true, note: target.label }); + } + + const updated = changes.filter((c) => c.changed); + const skipped = changes.filter((c) => !c.changed); + + console.log(`${dryRun ? '[dry-run] ' : ''}目标版本 ${version}`); + for (const c of updated) { + console.log(` 改 ${c.file}${c.note ? ` (${c.note})` : ''} ${c.from} → ${c.to}`); + } + for (const c of skipped) { + console.log(` 跳过 ${c.file}${c.note ? ` (${c.note})` : ''} 已是 ${c.from}`); + } + console.log(updated.length ? `共 ${updated.length} 处将写入。` : '没有需要修改的文件。'); +} + +main(); diff --git a/server/dukang-api/package.json b/server/dukang-api/package.json index 6d3fe1c..5f70bbe 100644 --- a/server/dukang-api/package.json +++ b/server/dukang-api/package.json @@ -1,6 +1,6 @@ { "name": "@dukang/api", - "version": "4.0.18", + "version": "4.0.19", "private": true, "scripts": { "predev": "pnpm --dir ../../packages/domain build", 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 5a18e59..547fd76 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 @@ -152,7 +152,7 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [ group: G.wechat_mini, type: 'string', requiresRestart: false, - placeholder: '4.0.4', + placeholder: '4.0.19', description: 'semver 格式(可带或不带 v);客户端低于此版本时提示更新', }, { diff --git a/server/dukang-api/src/integrations/wecom/wecom-message-push.service.ts b/server/dukang-api/src/integrations/wecom/wecom-message-push.service.ts index 03ee964..21aba43 100644 --- a/server/dukang-api/src/integrations/wecom/wecom-message-push.service.ts +++ b/server/dukang-api/src/integrations/wecom/wecom-message-push.service.ts @@ -483,6 +483,7 @@ export class WecomMessagePushService implements OnModuleInit { userName: '好客用户', userPhone: '13800008000', userRemark: '大客户', + userSource: '推广码 · 秋季品鉴', phoneMasked: '13800008000', }, handlePath: '/orders?orderNo=DK202608200001', diff --git a/server/dukang-api/src/integrations/wecom/wecom-push-template.defaults.test.ts b/server/dukang-api/src/integrations/wecom/wecom-push-template.defaults.test.ts new file mode 100644 index 0000000..4ca1110 --- /dev/null +++ b/server/dukang-api/src/integrations/wecom/wecom-push-template.defaults.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { WECOM_PUSH_TEMPLATE_DEFAULTS, WECOM_PUSH_TEMPLATE_LEGACY_BODIES } from './wecom-push-template.defaults'; + +describe('wecom-push-template.defaults', () => { + it('order.paid 默认正文在备注下含用户来源', () => { + const paid = WECOM_PUSH_TEMPLATE_DEFAULTS.find((t) => t.eventKey === 'order.paid'); + expect(paid?.body).toContain('备注:{{userRemark}}'); + expect(paid?.body).toContain('用户来源:{{userSource}}'); + expect(paid?.body.indexOf('备注:{{userRemark}}') ?? -1).toBeLessThan( + paid?.body.indexOf('用户来源:{{userSource}}') ?? -1, + ); + }); + + it('上一版含备注的默认文案列入 legacy 以便启动升级', () => { + const legacy = WECOM_PUSH_TEMPLATE_LEGACY_BODIES['order.paid'] ?? []; + expect(legacy.some((body) => body.includes('备注:{{userRemark}}') && !body.includes('用户来源'))).toBe( + true, + ); + }); +}); diff --git a/server/dukang-api/src/integrations/wecom/wecom-push-template.defaults.ts b/server/dukang-api/src/integrations/wecom/wecom-push-template.defaults.ts index 613967d..0c480c4 100644 --- a/server/dukang-api/src/integrations/wecom/wecom-push-template.defaults.ts +++ b/server/dukang-api/src/integrations/wecom/wecom-push-template.defaults.ts @@ -22,6 +22,7 @@ export const WECOM_PUSH_TEMPLATE_DEFAULTS: WecomTemplateDefault[] = [ '收货:{{receiverInfo}}', '用户:{{userName}} {{userPhone}}', '备注:{{userRemark}}', + '用户来源:{{userSource}}', '时间:{{time}}', '[{{handleLabel}}]({{handleUrl}})', ].join('\n'), @@ -227,6 +228,19 @@ export const WECOM_PUSH_TEMPLATE_LEGACY_BODIES: Partial