Files
FlashSaleSystem/flash-sale-frontend/src/utils/normalizers.ts
YoVinchen 098ea9ad02 fix: 修复商品图片管理和缓存一致性问题
- 重构 ImageUpload 组件,使用 rawUrl 跟踪原始路径
- 新增 normalizeStorageImageUrl 避免存储绝对 URL
- 商品增删改后同步清除缓存和旧图片文件
- 修复秒杀活动列表商品图片为空的问题
2026-03-14 21:05:42 +08:00

357 lines
14 KiB
TypeScript

import type {
CartItem,
FlashSale,
GroupBuying,
GroupBuyingGroup,
Order,
OrderAddress,
PageResponse,
Product,
User,
} from '@/types/api'
import type {
AdminHotProductRow,
AdminOrderRow,
AdminProductRow,
AdminRecentOrderRow,
AdminUserRow,
} from '@/types/admin'
import { DEFAULT_PRODUCT_IMAGE, normalizeStorageImageUrl, resolveImageUrl } from '@/utils/image'
const toNumber = (value: unknown, fallback = 0) => {
const result = Number(value)
return Number.isFinite(result) ? result : fallback
}
const toString = (value: unknown, fallback = '') => {
if (value === null || value === undefined) {
return fallback
}
return String(value)
}
const toIsoLikeString = (value: unknown) => {
const raw = toString(value)
return raw || new Date().toISOString()
}
export const buildOrderNo = (id: number | string) => {
const numericId = toString(id).padStart(6, '0')
return `FS${numericId}`
}
export const mapUserStatusText = (status: number) => {
return status === 1 ? '正常' : '禁用'
}
export const mapOrderStatus = (status: number | string): Order['status'] => {
const value = typeof status === 'string' ? status : toNumber(status)
if (value === 'PENDING' || value === 1) return 'PENDING'
if (value === 'PAID' || value === 2) return 'PAID'
if (value === 'SHIPPED' || value === 3) return 'SHIPPED'
if (value === 'COMPLETED' || value === 4) return 'COMPLETED'
if (value === 'CANCELLED' || value === 5) return 'CANCELLED'
return 'PENDING'
}
export const mapFlashSaleStatus = (status: number | string): FlashSale['status'] => {
const value = typeof status === 'string' ? status : toNumber(status)
if (value === 'UPCOMING' || value === 1) return 'UPCOMING'
if (value === 'ACTIVE' || value === 2) return 'ACTIVE'
if (value === 'ENDED' || value === 3) return 'ENDED'
if (value === 'PAUSED' || value === 4) return 'PAUSED'
return 'UPCOMING'
}
export const mapProductStatus = (status: number | string, stock = 0): Product['status'] => {
const value = typeof status === 'string' ? status : toNumber(status)
if (stock <= 0) return 'SOLD_OUT'
if (value === 'OFF_SALE' || value === 0) return 'OFF_SALE'
return 'ON_SALE'
}
export const normalizeUser = (user: Record<string, any>): User => {
const username = toString(user.username)
return {
id: toNumber(user.id),
username,
email: toString(user.email),
phone: toString(user.phone),
avatar: resolveImageUrl(toString(user.avatar, '')),
role: toString(user.role).toUpperCase() === 'ADMIN' ? 'ADMIN' : username === 'admin' ? 'ADMIN' : 'USER',
status: toNumber(user.status, 1) === 1 ? 'ACTIVE' : 'BANNED',
createdAt: toIsoLikeString(user.createdAt),
updatedAt: toIsoLikeString(user.updatedAt || user.createdAt),
}
}
export const normalizeProduct = (product: Record<string, any>): Product => {
const stock = toNumber(product.stock)
const imageUrl = resolveImageUrl(toString(product.imageUrl, ''))
return {
id: toNumber(product.id),
name: toString(product.name),
description: toString(product.description),
price: toNumber(product.price),
stock,
imageUrl,
images: imageUrl ? [imageUrl] : [DEFAULT_PRODUCT_IMAGE],
category: toString(product.category, '默认分类'),
status: mapProductStatus(product.status, stock),
sales: toNumber(product.sales),
views: toNumber(product.viewCount ?? product.views),
createdAt: toIsoLikeString(product.createdAt),
updatedAt: toIsoLikeString(product.updatedAt || product.createdAt),
}
}
export const normalizeFlashSale = (flashSale: Record<string, any>): FlashSale => {
const flashStock = toNumber(flashSale.flashStock)
const remainingStock = toNumber(flashSale.remainingStock, flashStock)
return {
id: toNumber(flashSale.id),
productId: toNumber(flashSale.productId),
productName: toString(flashSale.productName),
productImageUrl: resolveImageUrl(toString(flashSale.productImageUrl, '')),
originalPrice: toNumber(flashSale.originalPrice),
flashPrice: toNumber(flashSale.flashPrice),
flashStock,
remainingStock,
startTime: toIsoLikeString(flashSale.startTime),
endTime: toIsoLikeString(flashSale.endTime),
status: mapFlashSaleStatus(flashSale.status),
limitPerUser: toNumber(flashSale.limitPerUser, 1),
description: toString(flashSale.description || flashSale.statusDescription),
createdAt: toIsoLikeString(flashSale.createdAt),
updatedAt: toIsoLikeString(flashSale.updatedAt || flashSale.createdAt),
}
}
const buildOrderAddress = (order: Record<string, any>): OrderAddress | undefined => {
const name = toString(order.receiverName)
const phone = toString(order.receiverPhone)
const address = toString(order.receiverAddress)
if (!name && !phone && !address) {
return undefined
}
return {
name,
phone,
province: '',
city: '',
district: '',
address,
}
}
export const normalizeOrder = (order: Record<string, any>): Order => {
const totalAmount = toNumber(order.totalAmount ?? order.totalPrice)
const quantity = toNumber(order.quantity, 1)
const status = mapOrderStatus(order.status)
const createdAt = toIsoLikeString(order.createdAt)
const updatedAt = toIsoLikeString(order.updatedAt || order.createdAt)
const productImage = resolveImageUrl(toString(order.productImageUrl, ''))
const fallbackItem = {
id: toNumber(order.productId || order.id),
productId: toNumber(order.productId),
productName: toString(order.productName, '未知商品'),
productImage,
price: quantity > 0 ? Number((totalAmount / quantity).toFixed(2)) : totalAmount,
quantity,
subtotal: totalAmount,
}
const items = Array.isArray(order.items) && order.items.length > 0
? order.items.map((item: Record<string, any>) => ({
id: toNumber(item.id || item.productId),
productId: toNumber(item.productId),
productName: toString(item.productName, '未知商品'),
productImage: resolveImageUrl(toString(item.productImageUrl || item.productImage, '')),
price: toNumber(item.price),
quantity: toNumber(item.quantity, 1),
subtotal: toNumber(item.subtotal ?? item.price),
}))
: [fallbackItem]
return {
id: toNumber(order.id),
orderNo: toString(order.orderNo, buildOrderNo(order.id)),
userId: toNumber(order.userId),
username: toString(order.username),
totalAmount,
paymentAmount: totalAmount,
paymentMethod: toString(order.paymentMethod) || (status === 'PENDING' ? undefined : 'ONLINE'),
status,
items,
address: buildOrderAddress(order),
remark: toString(order.remark),
createdAt,
updatedAt,
paidAt: order.paidAt ? toIsoLikeString(order.paidAt) : (status === 'PAID' || status === 'SHIPPED' || status === 'COMPLETED' ? updatedAt : undefined),
shippedAt: order.shippedAt ? toIsoLikeString(order.shippedAt) : (status === 'SHIPPED' || status === 'COMPLETED' ? updatedAt : undefined),
completedAt: order.completedAt ? toIsoLikeString(order.completedAt) : (status === 'COMPLETED' ? updatedAt : undefined),
}
}
export const normalizeCartItems = (cart: Record<string, any> | undefined): CartItem[] => {
const items = Array.isArray(cart?.items) ? cart.items : []
return items.map((item: Record<string, any>) => ({
id: toString(item.productId),
productId: toNumber(item.productId),
productName: toString(item.productName),
productImage: resolveImageUrl(toString(item.productImageUrl || item.productImage, '')),
price: toNumber(item.productPrice),
quantity: toNumber(item.quantity, 1),
stock: toNumber(item.stock),
selected: true,
createdAt: new Date().toISOString(),
}))
}
export const normalizePage = <T>(payload: Record<string, any>, mapper: (item: Record<string, any>) => T): PageResponse<T> => {
const content = Array.isArray(payload.content) ? payload.content.map((item: Record<string, any>) => mapper(item)) : []
const size = toNumber(payload.size, content.length || 10)
const pageNumber = toNumber(payload.currentPage ?? payload.number)
const totalElements = toNumber(payload.totalElements, content.length)
const totalPages = toNumber(payload.totalPages, size > 0 ? Math.ceil(totalElements / size) : 1)
return {
content,
totalElements,
totalPages,
size,
number: pageNumber,
first: pageNumber <= 0,
last: totalPages === 0 ? true : pageNumber >= totalPages - 1,
}
}
export const normalizeAdminRecentOrder = (order: Record<string, any>): AdminRecentOrderRow => ({
id: toNumber(order.id),
orderNo: buildOrderNo(order.id),
username: toString(order.username),
productName: toString(order.productName),
quantity: toNumber(order.quantity, 1),
totalAmount: toNumber(order.totalAmount ?? order.totalPrice),
status: mapOrderStatus(order.status),
createdAt: toIsoLikeString(order.createdAt),
isFlashSale: Boolean(order.isFlashSale),
})
export const normalizeAdminHotProduct = (product: Record<string, any>): AdminHotProductRow => ({
id: toNumber(product.id),
name: toString(product.name),
price: toNumber(product.price),
stock: toNumber(product.stock),
sales: toNumber(product.sales),
})
export const normalizeAdminUser = (user: Record<string, any>): AdminUserRow => ({
id: toNumber(user.id),
username: toString(user.username),
email: toString(user.email),
phone: toString(user.phone),
status: toNumber(user.status, 1),
statusText: mapUserStatusText(toNumber(user.status, 1)),
role: toString(user.role).toUpperCase() === 'ADMIN' || toString(user.username) === 'admin' ? 'ADMIN' : 'USER',
isOnline: Boolean(user.isOnline),
createdAt: toIsoLikeString(user.createdAt),
lastLogin: user.lastLogin ? toIsoLikeString(user.lastLogin) : undefined,
})
export const normalizeAdminOrder = (order: Record<string, any>): AdminOrderRow => ({
id: toNumber(order.id),
orderNo: buildOrderNo(order.id),
username: toString(order.username),
productName: toString(order.productName),
productId: toNumber(order.productId),
quantity: toNumber(order.quantity, 1),
totalAmount: toNumber(order.totalAmount),
status: mapOrderStatus(order.status),
createdAt: toIsoLikeString(order.createdAt),
isFlashSale: Boolean(order.isFlashSale),
})
export const normalizeAdminProduct = (product: Record<string, any>): AdminProductRow => ({
id: toNumber(product.id),
name: toString(product.name),
description: toString(product.description),
category: toString(product.category, '默认分类'),
price: toNumber(product.price),
stock: toNumber(product.stock),
status: toNumber(product.status, 1),
imageUrl: normalizeStorageImageUrl(toString(product.imageUrl, '')),
createdAt: toIsoLikeString(product.createdAt),
updatedAt: product.updatedAt ? toIsoLikeString(product.updatedAt) : undefined,
totalSales: toNumber(product.totalSales),
totalRevenue: toNumber(product.totalRevenue),
viewCount: toNumber(product.viewCount),
rating: toNumber(product.rating),
})
export const mapGroupBuyingStatus = (status: number | string): GroupBuying['status'] => {
const value = typeof status === 'string' ? status : toNumber(status)
if (value === 'DRAFT' || value === 0) return 'DRAFT'
if (value === 'UPCOMING' || value === 1) return 'UPCOMING'
if (value === 'ACTIVE' || value === 2) return 'ACTIVE'
if (value === 'ENDED' || value === 3) return 'ENDED'
return 'DRAFT'
}
export const mapGroupStatus = (status: number | string): GroupBuyingGroup['status'] => {
const value = typeof status === 'string' ? status : toNumber(status)
if (value === 'FORMING' || value === 1) return 'FORMING'
if (value === 'SUCCESS' || value === 2) return 'SUCCESS'
if (value === 'FAILED' || value === 3) return 'FAILED'
return 'FORMING'
}
export const normalizeGroupBuying = (gb: Record<string, any>): GroupBuying => ({
id: toNumber(gb.id),
productId: toNumber(gb.productId),
productName: toString(gb.productName),
productImageUrl: resolveImageUrl(toString(gb.productImageUrl, '')),
productPrice: toNumber(gb.productPrice),
groupPrice: toNumber(gb.groupPrice),
requiredMembers: toNumber(gb.requiredMembers, 2),
durationMinutes: toNumber(gb.durationMinutes, 1440),
totalStock: toNumber(gb.totalStock),
remainingStock: toNumber(gb.remainingStock),
maxPerUser: toNumber(gb.maxPerUser, 1),
status: mapGroupBuyingStatus(gb.status),
statusDescription: toString(gb.statusDescription),
startTime: toIsoLikeString(gb.startTime),
endTime: toIsoLikeString(gb.endTime),
createdAt: toIsoLikeString(gb.createdAt),
updatedAt: toIsoLikeString(gb.updatedAt || gb.createdAt),
activeGroupCount: toNumber(gb.activeGroupCount),
discount: toNumber(gb.discount),
})
export const normalizeGroupBuyingGroup = (group: Record<string, any>): GroupBuyingGroup => ({
id: toNumber(group.id),
groupNo: toString(group.groupNo),
groupBuyingId: toNumber(group.groupBuyingId),
leaderUserId: toNumber(group.leaderUserId),
leaderUsername: toString(group.leaderUsername),
requiredMembers: toNumber(group.requiredMembers, 2),
currentMembers: toNumber(group.currentMembers, 1),
status: mapGroupStatus(group.status),
statusDescription: toString(group.statusDescription),
expireTime: toIsoLikeString(group.expireTime),
createdAt: toIsoLikeString(group.createdAt),
completedAt: group.completedAt ? toIsoLikeString(group.completedAt) : undefined,
members: Array.isArray(group.members)
? group.members.map((m: Record<string, any>) => ({
id: toNumber(m.id),
userId: toNumber(m.userId),
username: toString(m.username),
avatar: resolveImageUrl(toString(m.avatar, '')),
orderId: m.orderId ? toNumber(m.orderId) : undefined,
status: toNumber(m.status),
joinedAt: toIsoLikeString(m.joinedAt),
}))
: [],
groupBuying: group.groupBuying ? normalizeGroupBuying(group.groupBuying) : undefined,
})