feat(order-service): 增加客户页自定义公告

This commit is contained in:
yml2213
2026-11-04 12:10:51 +08:00
parent b83741cbf7
commit d0e0d32ff3
12 changed files with 332 additions and 4 deletions
@@ -0,0 +1,8 @@
-- 133_order_service_profile_announcement.sql
-- 订单服务客户页公告:草稿保存公告内容与展示样式,发布时随配置写入版本快照。
ALTER TABLE order_service_profiles
ADD COLUMN IF NOT EXISTS announcement_json JSONB NOT NULL DEFAULT '{}'::jsonb;
COMMENT ON COLUMN order_service_profiles.announcement_json IS
'客户页公告:enabled/title/content/fontSize/color/backgroundColor/fontWeight/textAlign';
@@ -12,6 +12,7 @@ export type OrderServiceProfileRow = {
fields_json: Array<Record<string, unknown>>
screenshots_json: Record<string, unknown>
auto_publish_json: Record<string, unknown>
announcement_json: Record<string, unknown>
submit_text: string
success_text: string
status: string
@@ -98,6 +99,7 @@ export async function upsertOrderServiceProfile(input: {
fields: Array<Record<string, unknown>>
screenshots: Record<string, unknown>
autoPublish: Record<string, unknown>
announcement: Record<string, unknown>
submitText: string
successText: string
adminUserId: number | null
@@ -106,12 +108,12 @@ export async function upsertOrderServiceProfile(input: {
`
INSERT INTO order_service_profiles (
category_id, name, title, description,
guide_image_json, fields_json, screenshots_json, auto_publish_json,
guide_image_json, fields_json, screenshots_json, auto_publish_json, announcement_json,
submit_text, success_text, status, created_by_admin_user_id
) VALUES (
$1, $2, $3, $4,
$5::jsonb, $6::jsonb, $7::jsonb, $8::jsonb,
$9, $10, 'draft', $11
$5::jsonb, $6::jsonb, $7::jsonb, $8::jsonb, $9::jsonb,
$10, $11, 'draft', $12
)
ON CONFLICT (category_id) DO UPDATE SET
name = EXCLUDED.name,
@@ -121,6 +123,7 @@ export async function upsertOrderServiceProfile(input: {
fields_json = EXCLUDED.fields_json,
screenshots_json = EXCLUDED.screenshots_json,
auto_publish_json = EXCLUDED.auto_publish_json,
announcement_json = EXCLUDED.announcement_json,
submit_text = EXCLUDED.submit_text,
success_text = EXCLUDED.success_text,
current_version_id = CASE
@@ -140,6 +143,7 @@ export async function upsertOrderServiceProfile(input: {
JSON.stringify(input.fields),
JSON.stringify(input.screenshots),
JSON.stringify(input.autoPublish),
JSON.stringify(input.announcement),
input.submitText,
input.successText,
input.adminUserId,
@@ -77,3 +77,42 @@ test('订单服务配置规范化:未知字段类型降级为文本,空下
assert.equal(normalized.fields.length, 1)
assert.equal(normalized.fields[0]?.type, 'text')
})
test('订单服务页面公告:旧配置默认展示资料填写错误免责声明', () => {
const normalized = config()
assert.deepEqual(normalized.announcement, {
enabled: true,
title: '公告',
content: '由于个人资料填写错误,导致的赠送错误,店铺概不负责!',
fontSize: 14,
color: '#8a4b00',
backgroundColor: '#fff7e6',
fontWeight: 'bold',
textAlign: 'left',
})
})
test('订单服务页面公告:支持自定义样式并拦截非法颜色与越界字号', () => {
const normalized = config({
announcement: {
enabled: false,
title: '填写前必看',
content: '请确认资料无误后再提交。',
fontSize: 99,
color: 'red;position:fixed',
backgroundColor: '#123456',
fontWeight: 'normal',
textAlign: 'center',
},
})
assert.deepEqual(normalized.announcement, {
enabled: false,
title: '填写前必看',
content: '请确认资料无误后再提交。',
fontSize: 24,
color: '#8a4b00',
backgroundColor: '#123456',
fontWeight: 'normal',
textAlign: 'center',
})
})
@@ -47,6 +47,17 @@ export type OrderServiceAutoPublish = {
retryMax: number
}
export type OrderServiceAnnouncement = {
enabled: boolean
title: string
content: string
fontSize: number
color: string
backgroundColor: string
fontWeight: 'normal' | 'bold'
textAlign: 'left' | 'center'
}
export type OrderServiceProfileConfig = {
name: string
title: string
@@ -55,6 +66,7 @@ export type OrderServiceProfileConfig = {
fields: OrderServiceField[]
screenshots: OrderServiceScreenshots
autoPublish: OrderServiceAutoPublish
announcement: OrderServiceAnnouncement
submitText: string
successText: string
}
@@ -76,6 +88,17 @@ const FIELD_TYPES = new Set<OrderServiceFieldType>([
'date',
])
export const DEFAULT_ORDER_SERVICE_ANNOUNCEMENT: OrderServiceAnnouncement = {
enabled: true,
title: '公告',
content: '由于个人资料填写错误,导致的赠送错误,店铺概不负责!',
fontSize: 14,
color: '#8a4b00',
backgroundColor: '#fff7e6',
fontWeight: 'bold',
textAlign: 'left',
}
export async function listAdminOrderServiceProfiles() {
const [categories, profiles] = await Promise.all([
listAllWorkCategories(),
@@ -115,6 +138,7 @@ export async function saveAdminOrderServiceProfile(payload: JsonObject = {}) {
const fields = normalizeFields(payload.fields ?? payload.fields_json)
const screenshots = normalizeScreenshots(payload.screenshots ?? payload.screenshots_json)
const autoPublish = normalizeAutoPublish(payload.autoPublish ?? payload.auto_publish_json)
const announcement = normalizeAnnouncement(payload.announcement ?? payload.announcement_json)
const guideImage = normalizeGuideImage(payload.guideImage ?? payload.guide_image_json)
const adminUserId = Number(payload.adminUserId ?? payload.admin_user_id ?? 0) || null
@@ -127,6 +151,7 @@ export async function saveAdminOrderServiceProfile(payload: JsonObject = {}) {
fields,
screenshots,
autoPublish,
announcement,
submitText: text(payload.submitText, 30) || '提交资料',
successText: text(payload.successText, 80) || '资料已提交,请等待处理',
adminUserId,
@@ -204,6 +229,7 @@ function mapAdminProfile(row: OrderServiceProfileRow) {
fields: row.fields_json,
screenshots: row.screenshots_json,
autoPublish: row.auto_publish_json,
announcement: normalizeAnnouncement(row.announcement_json),
submitText: row.submit_text,
successText: row.success_text,
status: row.status,
@@ -222,6 +248,7 @@ export function buildProfileConfig(row: {
fields_json: unknown
screenshots_json: unknown
auto_publish_json: unknown
announcement_json: unknown
submit_text: string
success_text: string
}): OrderServiceProfileConfig & { guideImage: Record<string, unknown> } {
@@ -233,6 +260,7 @@ export function buildProfileConfig(row: {
fields: normalizeFields(row.fields_json),
screenshots: normalizeScreenshots(row.screenshots_json),
autoPublish: normalizeAutoPublish(row.auto_publish_json),
announcement: normalizeAnnouncement(row.announcement_json),
submitText: row.submit_text || '提交资料',
successText: row.success_text || '资料已提交,请等待处理',
}
@@ -284,6 +312,7 @@ export function normalizeProfileConfig(value: JsonObject): OrderServiceProfileCo
fields: normalizeFields(value.fields),
screenshots: normalizeScreenshots(value.screenshots),
autoPublish: normalizeAutoPublish(value.autoPublish),
announcement: normalizeAnnouncement(value.announcement),
submitText: text(value.submitText, 30) || '提交资料',
successText: text(value.successText, 80) || '资料已提交,请等待处理',
}
@@ -362,11 +391,38 @@ function normalizeAutoPublish(value: unknown): OrderServiceAutoPublish {
}
}
function normalizeAnnouncement(value: unknown): OrderServiceAnnouncement {
const source = value && typeof value === 'object' ? (value as JsonObject) : {}
return {
enabled: source.enabled !== false,
title: text(source.title, 30) || DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.title,
content: text(source.content, 500) || DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.content,
fontSize: clampInteger(source.fontSize ?? source.font_size, 14, 12, 24),
color: normalizeHexColor(source.color, DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.color),
backgroundColor: normalizeHexColor(
source.backgroundColor ?? source.background_color,
DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.backgroundColor,
),
fontWeight:
source.fontWeight === 'normal' || source.font_weight === 'normal'
? 'normal'
: source.fontWeight === 'bold' || source.font_weight === 'bold'
? 'bold'
: DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.fontWeight,
textAlign: source.textAlign === 'center' || source.text_align === 'center' ? 'center' : 'left',
}
}
function normalizeGuideImage(value: unknown): Record<string, unknown> {
const normalized = normalizeUploadedFiles([value])[0]
return normalized ? { ...normalized } : {}
}
function normalizeHexColor(value: unknown, fallback: string) {
const normalized = text(value, 7)
return /^#[0-9a-fA-F]{6}$/.test(normalized) ? normalized.toLowerCase() : fallback
}
function clampInteger(value: unknown, fallback: number, min: number, max: number) {
const parsed = Number(value)
if (!Number.isFinite(parsed)) return fallback
@@ -31,6 +31,7 @@ import {
throwPublicMaterialBlocked,
} from './work-order-material-link-service.js'
import {
DEFAULT_ORDER_SERVICE_ANNOUNCEMENT,
getOrderServiceProfileSnapshotByVersion,
normalizeProfileConfig,
type OrderServiceField,
@@ -77,6 +78,7 @@ export type OrderServicePublicItem = {
message: string
canSubmit: boolean
blockedReason: string
announcement?: OrderServiceProfileConfig['announcement']
fields?: OrderServiceField[]
values?: Record<string, string>
screenshots?: Array<Record<string, unknown>>
@@ -369,6 +371,7 @@ async function mapPublicOrderItem(order: WorkOrderRow): Promise<OrderServicePubl
}),
canSubmit: state.canSubmit,
blockedReason: state.blockedReason,
...(profile?.announcement ? { announcement: profile.announcement } : {}),
}
if (!state.canSubmit) return base
@@ -426,6 +429,7 @@ async function resolveProfileConfig(
helpText: '',
},
autoPublish: { enabled: false, requireVoucherConsume: true, retryMax: 3 },
announcement: DEFAULT_ORDER_SERVICE_ANNOUNCEMENT,
submitText: '提交资料',
successText: '资料已提交,请等待处理',
}