43c7c03aee
GET /api/admin/skip-price-asins 列表解析(SkipPriceAsinPageVo, 每店铺 5 国家 ASIN+最低价) + 分组/店铺名/ASIN/国家/最低价区间筛选 snake 序列化, 新增共享国家字典, 9 个契约测试。
132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
/** 最低价 ASIN 列表模型(任务 75):解析 SkipPriceAsinPageVo(camel) 宽行(每店铺 5 国家 ASIN+最低价),纯逻辑。 */
|
||
import { unwrap } from '../../api/envelope.ts'
|
||
import { ASIN_PAGE_DEFAULT_SIZE } from './asin-filter.ts'
|
||
import { ASIN_COUNTRY_CODES, type AsinCountryCode } from './asin-country.ts'
|
||
|
||
export interface SkipPriceItem {
|
||
id: number
|
||
groupId: number | null
|
||
groupName: string
|
||
shopName: string
|
||
asinDe: string
|
||
minimumPriceDe: number | null
|
||
asinUk: string
|
||
minimumPriceUk: number | null
|
||
asinFr: string
|
||
minimumPriceFr: number | null
|
||
asinIt: string
|
||
minimumPriceIt: number | null
|
||
asinEs: string
|
||
minimumPriceEs: number | null
|
||
createdAt?: string
|
||
updatedAt?: string
|
||
}
|
||
|
||
export interface SkipPricePageResult {
|
||
items: SkipPriceItem[]
|
||
total: number
|
||
page: number
|
||
pageSize: number
|
||
}
|
||
|
||
export function emptySkipPricePage(): SkipPricePageResult {
|
||
return { items: [], total: 0, page: 1, pageSize: ASIN_PAGE_DEFAULT_SIZE }
|
||
}
|
||
|
||
function text(value: unknown): string {
|
||
return typeof value === 'string' ? value.trim() : ''
|
||
}
|
||
|
||
/** 数字或可解析数字串 → number;否则 null(BigDecimal 可能以字符串下发)。 */
|
||
function numberOrNull(value: unknown): number | null {
|
||
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||
if (typeof value === 'string') {
|
||
const trimmed = value.trim()
|
||
if (trimmed && Number.isFinite(Number(trimmed))) return Number(trimmed)
|
||
}
|
||
return null
|
||
}
|
||
|
||
function idOrNull(value: unknown): number | null {
|
||
const num = numberOrNull(value)
|
||
return num === null ? null : Math.floor(num)
|
||
}
|
||
|
||
function lowerCode(code: AsinCountryCode): string {
|
||
return code.toLowerCase()
|
||
}
|
||
|
||
/** 'DE' → 'De',拼出 VO 驼峰字段后缀(asinDe/minimumPriceDe)。 */
|
||
function countrySuffix(code: string): string {
|
||
return code.length > 1 ? code[0] + code.slice(1).toLowerCase() : code.toUpperCase()
|
||
}
|
||
|
||
/** 解析单条最低价 ASIN 店铺行;缺 id 视为无效。 */
|
||
export function toSkipPriceItem(raw: unknown): SkipPriceItem | null {
|
||
if (!raw || typeof raw !== 'object') return null
|
||
const record = raw as Record<string, unknown>
|
||
const id = idOrNull(record.id)
|
||
if (id === null) return null
|
||
const item: SkipPriceItem = {
|
||
id,
|
||
groupId: idOrNull(record.groupId ?? record.group_id),
|
||
groupName: text(record.groupName ?? record.group_name),
|
||
shopName: text(record.shopName ?? record.shop_name),
|
||
asinDe: '',
|
||
minimumPriceDe: null,
|
||
asinUk: '',
|
||
minimumPriceUk: null,
|
||
asinFr: '',
|
||
minimumPriceFr: null,
|
||
asinIt: '',
|
||
minimumPriceIt: null,
|
||
asinEs: '',
|
||
minimumPriceEs: null,
|
||
}
|
||
const writable = item as unknown as Record<string, unknown>
|
||
for (const code of ASIN_COUNTRY_CODES) {
|
||
const suffix = countrySuffix(code)
|
||
const camelAsin = `asin${suffix}`
|
||
const snakeAsin = `asin_${lowerCode(code)}`
|
||
writable[camelAsin] = text(record[camelAsin] ?? record[snakeAsin])
|
||
const camelPrice = `minimumPrice${suffix}`
|
||
const snakePrice = `minimum_price_${lowerCode(code)}`
|
||
writable[camelPrice] = numberOrNull(record[camelPrice] ?? record[snakePrice])
|
||
}
|
||
const createdAt = text(record.createdAt ?? record.created_at)
|
||
if (createdAt) item.createdAt = createdAt
|
||
const updatedAt = text(record.updatedAt ?? record.updated_at)
|
||
if (updatedAt) item.updatedAt = updatedAt
|
||
return item
|
||
}
|
||
|
||
/** 归一化分页负载(信封或已解包 VO)为前端结果;缺省字段回默认。 */
|
||
export function parseSkipPricePage(payload: unknown): SkipPricePageResult {
|
||
const out = emptySkipPricePage()
|
||
const core = unwrap<unknown>(payload)
|
||
if (!core || typeof core !== 'object') return out
|
||
const record = core as Record<string, unknown>
|
||
if (Array.isArray(record.items)) {
|
||
out.items = record.items
|
||
.map((raw) => toSkipPriceItem(raw))
|
||
.filter((item): item is SkipPriceItem => item !== null)
|
||
}
|
||
if (typeof record.total === 'number') out.total = Math.floor(record.total)
|
||
if (typeof record.page === 'number' && record.page >= 1) out.page = Math.floor(record.page)
|
||
const rawSize = record.pageSize ?? record.page_size
|
||
if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize)
|
||
return out
|
||
}
|
||
|
||
/** 取某国家列 ASIN;国家码不属白名单返回空串。 */
|
||
export function skipPriceAsin(item: SkipPriceItem, country: string): string {
|
||
if (!ASIN_COUNTRY_CODES.includes(country as AsinCountryCode)) return ''
|
||
return item[`asin${countrySuffix(country)}` as keyof SkipPriceItem] as string
|
||
}
|
||
|
||
/** 取某国家最低价;国家码不属白名单返回 null。 */
|
||
export function skipPriceMinimum(item: SkipPriceItem, country: string): number | null {
|
||
if (!ASIN_COUNTRY_CODES.includes(country as AsinCountryCode)) return null
|
||
return item[`minimumPrice${countrySuffix(country)}` as keyof SkipPriceItem] as number | null
|
||
}
|