merge(dev): sync to main
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/admin-web",
|
||||
"version": "4.0.18",
|
||||
"version": "4.0.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/h5-partner",
|
||||
"version": "4.0.18",
|
||||
"version": "4.0.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/h5-shop",
|
||||
"version": "4.0.18",
|
||||
"version": "4.0.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/mini-user",
|
||||
"version": "4.0.18",
|
||||
"version": "4.0.19",
|
||||
"private": true,
|
||||
"description": "杜康好客 · C 端用户微信小程序(Taro)",
|
||||
"scripts": {
|
||||
|
||||
@@ -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<string, string | undefined>): CheckoutContext {
|
||||
return {
|
||||
productId: params.productId,
|
||||
|
||||
@@ -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, '')}`;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<PageShell variant="sub" className="order-confirm-page" hasFixedFooter>
|
||||
<SubPageHeader title="现场取货确认" />
|
||||
<View className="sub-page-body">
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">取货方式</Text>
|
||||
<Text className="order-card-title">收货方式</Text>
|
||||
{allowOnline ? (
|
||||
<View className="order-fulfillment-switch">
|
||||
<View className="order-fulfillment-opt" onClick={goDelivery}>
|
||||
<Text>配送到家</Text>
|
||||
</View>
|
||||
<View className="order-fulfillment-opt order-fulfillment-opt--active">
|
||||
<Text>现场提货</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
<Text className="u-muted">现场取货 · 无需填写收货地址 · 免运费</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<PageShell variant="sub" className="order-confirm-page" hasFixedFooter>
|
||||
<SubPageHeader title="确认订单" />
|
||||
<View className="sub-page-body">
|
||||
<View
|
||||
className="order-card"
|
||||
onClick={() =>
|
||||
Taro.navigateTo({
|
||||
url: buildAddressListUrl({
|
||||
productId,
|
||||
qty: String(quantity),
|
||||
addressId,
|
||||
cross: forceCross,
|
||||
}),
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text className="order-card-title">收货地址</Text>
|
||||
{selectedAddress ? (
|
||||
<View>
|
||||
<View style={{ display: 'flex', gap: '8px', marginBottom: 4 }}>
|
||||
<Text className="order-card-title" style={{ fontSize: 15 }}>{selectedAddress.receiverName}</Text>
|
||||
<Text className="u-muted">{maskPhone(selectedAddress.phone)}</Text>
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">收货方式</Text>
|
||||
{allowPickup ? (
|
||||
<View className="order-fulfillment-switch">
|
||||
<View className="order-fulfillment-opt order-fulfillment-opt--active">
|
||||
<Text>配送到家</Text>
|
||||
</View>
|
||||
<View className="order-fulfillment-opt order-fulfillment-opt--pickup" onClick={goOnSitePickup}>
|
||||
<Text>现场提货</Text>
|
||||
</View>
|
||||
<Text className="u-muted">{formatAddress(selectedAddress)}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text className="u-muted">点击选择收货地址</Text>
|
||||
)}
|
||||
) : null}
|
||||
{allowPickup ? (
|
||||
<Text className="u-muted order-pickup-guide">人在门店?点「现场提货」,免填地址</Text>
|
||||
) : null}
|
||||
<View
|
||||
onClick={() =>
|
||||
Taro.navigateTo({
|
||||
url: buildAddressListUrl({
|
||||
productId,
|
||||
qty: String(quantity),
|
||||
addressId,
|
||||
cross: forceCross,
|
||||
}),
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text className="order-card-title order-address-title">收货地址</Text>
|
||||
{selectedAddress ? (
|
||||
<View>
|
||||
<View style={{ display: 'flex', gap: '8px', marginBottom: 4 }}>
|
||||
<Text className="order-card-title" style={{ fontSize: 15 }}>{selectedAddress.receiverName}</Text>
|
||||
<Text className="u-muted">{maskPhone(selectedAddress.phone)}</Text>
|
||||
</View>
|
||||
<Text className="u-muted">{formatAddress(selectedAddress)}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text className="u-muted">点击选择收货地址</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!addressOk && addressId ? (
|
||||
|
||||
@@ -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() {
|
||||
</View>
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">收货信息</Text>
|
||||
{canSwitchPickup ? (
|
||||
<View className="order-fulfillment-switch">
|
||||
<View className="order-fulfillment-opt order-fulfillment-opt--active">
|
||||
<Text>配送到家</Text>
|
||||
</View>
|
||||
<View
|
||||
className="order-fulfillment-opt order-fulfillment-opt--pickup"
|
||||
onClick={goOnSitePickup}
|
||||
>
|
||||
<Text>现场提货</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
{canSwitchPickup ? (
|
||||
<Text className="u-muted order-pickup-guide">人在门店?点「现场提货」重新下单,免填地址</Text>
|
||||
) : null}
|
||||
{receiverLine || addressText ? (
|
||||
<>
|
||||
{receiverLine ? (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -38,6 +38,18 @@ export const USER_SOURCE_TYPE_LABELS: Record<UserSourceType, string> = {
|
||||
[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',
|
||||
|
||||
@@ -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('结算账单模板含累计金额与明细摘要', () => {
|
||||
|
||||
@@ -200,6 +200,7 @@ export const WECOM_TEMPLATE_PLACEHOLDERS: Record<WecomTemplateEventKey, string[]
|
||||
'userName',
|
||||
'userPhone',
|
||||
'userRemark',
|
||||
'userSource',
|
||||
'phoneMasked',
|
||||
'time',
|
||||
'handleUrl',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/shared-ui",
|
||||
"version": "4.0.18",
|
||||
"version": "4.0.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 统一改产品版本号:各端 package.json + 小程序兜底 + API health + HQ 最低版本占位。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/set-version.mjs 4.0.19
|
||||
* node scripts/set-version.mjs v4.0.19
|
||||
* node scripts/set-version.mjs 4.0.19 --dry-run
|
||||
* pnpm set-version -- 4.0.19
|
||||
*
|
||||
* 不改内部库(packages/domain、shared-types、weixin-sdk、client-logging 仍为 0.1.0)。
|
||||
* 加 --include-libs 才会一并改这些库。
|
||||
*/
|
||||
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const LIB_PACKAGES = new Set([
|
||||
'packages/domain/package.json',
|
||||
'packages/shared-types/package.json',
|
||||
'packages/weixin-sdk/package.json',
|
||||
'packages/client-logging/package.json',
|
||||
]);
|
||||
|
||||
const SOURCE_TARGETS = [
|
||||
{
|
||||
file: 'apps/mini-user/src/lib/client-version.ts',
|
||||
label: '小程序版本兜底',
|
||||
pattern: /(\|\| ')(\d+\.\d+\.\d+)(')/,
|
||||
},
|
||||
{
|
||||
file: 'server/dukang-api/src/modules/health/health.controller.ts',
|
||||
label: 'API /health version',
|
||||
pattern: /(version:\s*')(\d+\.\d+\.\d+)(')/,
|
||||
},
|
||||
{
|
||||
file: 'server/dukang-api/src/common/system-config/system-config.registry.ts',
|
||||
label: 'HQ 小程序最低版本占位',
|
||||
pattern: /(placeholder:\s*')(\d+\.\d+\.\d+)(')/,
|
||||
},
|
||||
];
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const flags = new Set();
|
||||
const rest = [];
|
||||
for (const a of argv) {
|
||||
if (a === '--dry-run' || a === '-n') flags.add('dryRun');
|
||||
else if (a === '--include-libs') flags.add('includeLibs');
|
||||
else if (a === '--help' || a === '-h') flags.add('help');
|
||||
else if (a.startsWith('-')) {
|
||||
fail(`未知参数:${a}\n用法:node scripts/set-version.mjs <version> [--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 <version> [--dry-run] [--include-libs]
|
||||
pnpm set-version -- <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();
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dukang/api",
|
||||
"version": "4.0.18",
|
||||
"version": "4.0.19",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"predev": "pnpm --dir ../../packages/domain build",
|
||||
|
||||
@@ -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);客户端低于此版本时提示更新',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -483,6 +483,7 @@ export class WecomMessagePushService implements OnModuleInit {
|
||||
userName: '好客用户',
|
||||
userPhone: '13800008000',
|
||||
userRemark: '大客户',
|
||||
userSource: '推广码 · 秋季品鉴',
|
||||
phoneMasked: '13800008000',
|
||||
},
|
||||
handlePath: '/orders?orderNo=DK202608200001',
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<Record<WecomTemplateEven
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
[
|
||||
'**订单支付成功**',
|
||||
'订单号:{{orderNo}}',
|
||||
'实付:¥{{payAmount}}',
|
||||
'城市:{{cityName}}',
|
||||
'商品:{{skuSummary}}',
|
||||
'物流:{{deliveryLabel}}',
|
||||
'收货:{{receiverInfo}}',
|
||||
'用户:{{userName}} {{userPhone}}',
|
||||
'备注:{{userRemark}}',
|
||||
'时间:{{time}}',
|
||||
'[{{handleLabel}}]({{handleUrl}})',
|
||||
].join('\n'),
|
||||
],
|
||||
'redeem.success': [
|
||||
[
|
||||
|
||||
@@ -28,7 +28,7 @@ export class HealthController {
|
||||
return {
|
||||
status,
|
||||
service: 'dukang-api',
|
||||
version: 'prev1',
|
||||
version: '4.0.19',
|
||||
checks: { db, redis },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
validateMinPurchase,
|
||||
validateShippingAddress,
|
||||
} from '@dukang/domain';
|
||||
import { loadAppConfig, ClientApp, WECHAT_AUTH_REQUIRED } from '@dukang/shared-types';
|
||||
import { loadAppConfig, ClientApp, WECHAT_AUTH_REQUIRED, formatUserSourceLine } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
@@ -521,7 +521,7 @@ export class TradeService {
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
city: { select: { name: true } },
|
||||
user: { select: { phone: true, nickname: true, hqRemark: true } },
|
||||
user: { select: { phone: true, nickname: true, hqRemark: true, sourceType: true, sourceLabel: true } },
|
||||
},
|
||||
});
|
||||
if (!order) return;
|
||||
@@ -554,6 +554,7 @@ export class TradeService {
|
||||
const userName = order.user?.nickname?.trim() || '—';
|
||||
const userPhone = (order.user?.phone || '').trim() || '—';
|
||||
const userRemark = (order.user?.hqRemark || '').trim() || '—';
|
||||
const userSource = formatUserSourceLine(order.user?.sourceType, order.user?.sourceLabel);
|
||||
void this.wecomPush.dispatchEvent(
|
||||
'order.paid',
|
||||
{
|
||||
@@ -566,6 +567,7 @@ export class TradeService {
|
||||
userName,
|
||||
userPhone,
|
||||
userRemark,
|
||||
userSource,
|
||||
phoneMasked: userPhone,
|
||||
},
|
||||
{ handlePath: `/orders?orderNo=${encodeURIComponent(order.orderNo)}` },
|
||||
|
||||
Reference in New Issue
Block a user