- 新增数据库迁移脚本,为详情表添加四个价格相关字段 - 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则 - 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位 - 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明 - 更新小程序端详情页面,支持展示配置的参考价格信息 - 补充相关测试用例,更新API文档与类型定义
101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
import type { DetailCreate, DetailPriceUnit, WanfaRouteCreate } from "@/types";
|
|
|
|
export function createEmptyWanfaRoute(): WanfaRouteCreate {
|
|
return { title: "", subtitle: "", image: "", routeCount: 0, demandKeyword: "" };
|
|
}
|
|
|
|
export function compactWanfaRoute(draft: WanfaRouteCreate): WanfaRouteCreate {
|
|
return {
|
|
title: draft.title.trim(),
|
|
subtitle: draft.subtitle.trim(),
|
|
image: draft.image.trim(),
|
|
routeCount: Number.isFinite(Number(draft.routeCount)) ? Math.max(0, Number(draft.routeCount)) : 0,
|
|
demandKeyword: draft.demandKeyword.trim(),
|
|
};
|
|
}
|
|
|
|
export function createEmptyDetail(): DetailCreate {
|
|
return {
|
|
key: "",
|
|
eyebrow: "玩法路线",
|
|
duration: "",
|
|
title: "",
|
|
subtitle: "",
|
|
intro: "",
|
|
highlights: [],
|
|
included: [],
|
|
excluded: [],
|
|
notes: [],
|
|
gallery: [],
|
|
priceStartingValue: null,
|
|
priceUnit: null,
|
|
pricePeopleRange: "",
|
|
priceDescription: "",
|
|
conciergeAdvisorId: null,
|
|
isActive: true,
|
|
};
|
|
}
|
|
|
|
export function compactDetail(draft: DetailCreate): DetailCreate {
|
|
return {
|
|
key: draft.key.trim(),
|
|
eyebrow: draft.eyebrow.trim(),
|
|
duration: draft.duration.trim(),
|
|
title: draft.title.trim(),
|
|
subtitle: draft.subtitle.trim(),
|
|
intro: draft.intro.trim(),
|
|
highlights: compactList(draft.highlights),
|
|
included: compactList(draft.included),
|
|
excluded: compactList(draft.excluded),
|
|
notes: compactList(draft.notes),
|
|
gallery: compactList(draft.gallery),
|
|
priceStartingValue: normalizePriceStartingValue(draft.priceStartingValue),
|
|
priceUnit: normalizePriceUnit(draft.priceUnit),
|
|
pricePeopleRange: draft.pricePeopleRange?.trim() || null,
|
|
priceDescription: draft.priceDescription?.trim() || null,
|
|
conciergeAdvisorId: draft.conciergeAdvisorId?.trim() || null,
|
|
isActive: draft.isActive ?? true,
|
|
...(draft.sortOrder === undefined ? {} : { sortOrder: draft.sortOrder }),
|
|
};
|
|
}
|
|
|
|
export function validateWanfaWrite(route: WanfaRouteCreate, detail: DetailCreate) {
|
|
if (!route.title || !route.subtitle || !route.image || !route.demandKeyword) {
|
|
return "请完整填写路线标题、副标题、封面和需求关键词。";
|
|
}
|
|
if (!isHttpUrl(route.image)) return "路线封面必须是有效的 HTTP(S) URL。";
|
|
if (!Number.isInteger(route.routeCount) || route.routeCount < 0) {
|
|
return "路线数量必须是大于等于 0 的整数。";
|
|
}
|
|
if (!detail.eyebrow || !detail.duration || !detail.title || !detail.subtitle || !detail.intro) {
|
|
return "请完整填写详情眉标、时长、标题、副标题和详情介绍。";
|
|
}
|
|
if (detail.gallery.some((image) => !isHttpUrl(image))) return "详情图片必须是有效的 HTTP(S) URL。";
|
|
if (detail.priceStartingValue !== null && detail.priceStartingValue !== undefined && !detail.priceUnit) {
|
|
return "配置价格起始值时请选择价格单位。";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normalizePriceStartingValue(value: number | null | undefined) {
|
|
if (value === null || value === undefined || !Number.isFinite(Number(value))) return null;
|
|
return Math.max(0, Number(Number(value).toFixed(2)));
|
|
}
|
|
|
|
function normalizePriceUnit(value: DetailPriceUnit | null | undefined): DetailPriceUnit | null {
|
|
return value === "person" || value === "day" || value === "group" ? value : null;
|
|
}
|
|
|
|
function compactList(items: string[]) {
|
|
return items.map((item) => item.trim()).filter(Boolean);
|
|
}
|
|
|
|
function isHttpUrl(value: string) {
|
|
try {
|
|
const url = new URL(value);
|
|
return url.protocol === "http:" || url.protocol === "https:";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|