- 重构 ImageUpload 组件,使用 rawUrl 跟踪原始路径 - 新增 normalizeStorageImageUrl 避免存储绝对 URL - 商品增删改后同步清除缓存和旧图片文件 - 修复秒杀活动列表商品图片为空的问题
357 lines
14 KiB
TypeScript
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,
|
|
})
|