完成consumer端同步
This commit is contained in:
295
utils/bankUtils.uts
Normal file
295
utils/bankUtils.uts
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* bankUtils.uts
|
||||
* 银行卡工具集:BIN 识别、OCR 适配、银行列表、表单校验
|
||||
* 所有接口均与真实 OCR 厂商解耦,需接入时只修改 recognizeBankCardFromImage 内部实现
|
||||
*/
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 1. 银行列表数据源
|
||||
// ─────────────────────────────────────────────
|
||||
export type BankItem = {
|
||||
code: string // 内部 code
|
||||
name: string // 银行全称
|
||||
shortName: string // 简称,用于 UI 显示
|
||||
logo: string // 本地 logo 路径(没有则为空串,前端显示占位)
|
||||
}
|
||||
|
||||
export const BANK_LIST: BankItem[] = [
|
||||
{ code: 'ICBC', name: '中国工商银行', shortName: '工商银行', logo: '' },
|
||||
{ code: 'CCB', name: '中国建设银行', shortName: '建设银行', logo: '' },
|
||||
{ code: 'ABC', name: '中国农业银行', shortName: '农业银行', logo: '' },
|
||||
{ code: 'BOC', name: '中国银行', shortName: '中国银行', logo: '' },
|
||||
{ code: 'COMM', name: '交通银行', shortName: '交通银行', logo: '' },
|
||||
{ code: 'CMB', name: '招商银行', shortName: '招商银行', logo: '' },
|
||||
{ code: 'CITIC', name: '中信银行', shortName: '中信银行', logo: '' },
|
||||
{ code: 'SPDB', name: '浦发银行', shortName: '浦发银行', logo: '' },
|
||||
{ code: 'CEB', name: '光大银行', shortName: '光大银行', logo: '' },
|
||||
{ code: 'HXB', name: '华夏银行', shortName: '华夏银行', logo: '' },
|
||||
{ code: 'CMBC', name: '民生银行', shortName: '民生银行', logo: '' },
|
||||
{ code: 'PAB', name: '平安银行', shortName: '平安银行', logo: '' },
|
||||
{ code: 'GDB', name: '广发银行', shortName: '广发银行', logo: '' },
|
||||
{ code: 'BOBJ', name: '北京银行', shortName: '北京银行', logo: '' },
|
||||
{ code: 'SHRCB', name: '上海银行', shortName: '上海银行', logo: '' },
|
||||
{ code: 'PSBC', name: '中国邮政储蓄银行', shortName: '邮储银行', logo: '' },
|
||||
]
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 2. BIN → 银行名称映射(仅 6 位前缀)
|
||||
// 来源:公开 BIN 数据,仅做参考识别用途
|
||||
// ─────────────────────────────────────────────
|
||||
type BinEntry = {
|
||||
prefix: string
|
||||
bankCode: string
|
||||
}
|
||||
|
||||
const BIN_MAP: BinEntry[] = [
|
||||
// 工商银行
|
||||
{ prefix: '6212', bankCode: 'ICBC' },
|
||||
{ prefix: '6216', bankCode: 'ICBC' },
|
||||
{ prefix: '6217', bankCode: 'ICBC' },
|
||||
{ prefix: '6222', bankCode: 'ICBC' },
|
||||
// 建设银行
|
||||
{ prefix: '6217', bankCode: 'CCB' },
|
||||
{ prefix: '6227', bankCode: 'CCB' },
|
||||
{ prefix: '6236', bankCode: 'CCB' },
|
||||
// 农业银行
|
||||
{ prefix: '6225', bankCode: 'ABC' },
|
||||
{ prefix: '6228', bankCode: 'ABC' },
|
||||
{ prefix: '9559', bankCode: 'ABC' },
|
||||
// 中国银行
|
||||
{ prefix: '6013', bankCode: 'BOC' },
|
||||
{ prefix: '6259', bankCode: 'BOC' },
|
||||
{ prefix: '6539', bankCode: 'BOC' },
|
||||
// 交通银行
|
||||
{ prefix: '6222', bankCode: 'COMM' },
|
||||
{ prefix: '6230', bankCode: 'COMM' },
|
||||
{ prefix: '6266', bankCode: 'COMM' },
|
||||
// 招商银行
|
||||
{ prefix: '6214', bankCode: 'CMB' },
|
||||
{ prefix: '6225', bankCode: 'CMB' },
|
||||
{ prefix: '6245', bankCode: 'CMB' },
|
||||
// 中信银行
|
||||
{ prefix: '6217', bankCode: 'CITIC' },
|
||||
{ prefix: '4367', bankCode: 'CITIC' },
|
||||
// 浦发银行
|
||||
{ prefix: '6217', bankCode: 'SPDB' },
|
||||
{ prefix: '6216', bankCode: 'SPDB' },
|
||||
// 平安银行
|
||||
{ prefix: '6214', bankCode: 'PAB' },
|
||||
{ prefix: '6216', bankCode: 'PAB' },
|
||||
// 邮储银行
|
||||
{ prefix: '6210', bankCode: 'PSBC' },
|
||||
{ prefix: '6221', bankCode: 'PSBC' },
|
||||
{ prefix: '6230', bankCode: 'PSBC' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 根据卡号前缀匹配银行(优先匹配最长前缀)
|
||||
* @param cardNo 银行卡号(至少 6 位)
|
||||
* @returns BankItem | null
|
||||
*/
|
||||
export function detectBankByBin(cardNo: string): BankItem | null {
|
||||
const cleaned = cardNo.replace(/\s/g, '')
|
||||
if (cleaned.length < 6) return null
|
||||
const prefix6 = cleaned.substring(0, 6)
|
||||
const prefix4 = cleaned.substring(0, 4)
|
||||
|
||||
// 优先 6 位匹配
|
||||
for (let i = 0; i < BIN_MAP.length; i++) {
|
||||
if (BIN_MAP[i].prefix === prefix6) {
|
||||
return findBankByCode(BIN_MAP[i].bankCode)
|
||||
}
|
||||
}
|
||||
// 降级 4 位匹配
|
||||
for (let i = 0; i < BIN_MAP.length; i++) {
|
||||
if (BIN_MAP[i].prefix === prefix4) {
|
||||
return findBankByCode(BIN_MAP[i].bankCode)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function findBankByCode(code: string): BankItem | null {
|
||||
for (let i = 0; i < BANK_LIST.length; i++) {
|
||||
if (BANK_LIST[i].code === code) return BANK_LIST[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 3. 银行卡号格式化(每 4 位一空格,仅展示用)
|
||||
// ─────────────────────────────────────────────
|
||||
export function formatCardNo(raw: string): string {
|
||||
const digits = raw.replace(/\D/g, '')
|
||||
let result = ''
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
if (i > 0 && i % 4 === 0) result += ' '
|
||||
result += digits[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** 去除格式化空格,返回纯数字 */
|
||||
export function cleanCardNo(formatted: string): string {
|
||||
return formatted.replace(/\s/g, '')
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 4. 银行卡号脱敏(验证页展示用)
|
||||
// ─────────────────────────────────────────────
|
||||
export function maskCardNo(cardNo: string): string {
|
||||
const digits = cardNo.replace(/\s/g, '')
|
||||
if (digits.length < 8) return digits
|
||||
const prefix = digits.substring(0, 4)
|
||||
const suffix = digits.substring(digits.length - 4)
|
||||
return prefix + ' **** **** ' + suffix
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 5. Luhn 校验(可选,建议开启)
|
||||
// ─────────────────────────────────────────────
|
||||
export function luhnCheck(cardNo: string): boolean {
|
||||
const digits = cardNo.replace(/\s/g, '')
|
||||
if (digits.length < 13 || digits.length > 19) return false
|
||||
let sum = 0
|
||||
let alternate = false
|
||||
for (let i = digits.length - 1; i >= 0; i--) {
|
||||
const digitStr = digits.substring(i, i + 1)
|
||||
let n = parseInt(digitStr)
|
||||
if (alternate) {
|
||||
n *= 2
|
||||
if (n > 9) n -= 9
|
||||
}
|
||||
sum += n
|
||||
alternate = !alternate
|
||||
}
|
||||
return sum % 10 === 0
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 6. 手机号校验
|
||||
// ─────────────────────────────────────────────
|
||||
export function isValidPhone(phone: string): boolean {
|
||||
return /^1[3-9]\d{9}$/.test(phone)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 7. OCR 适配层(银行卡识别)
|
||||
// 【接入说明】真实接入时,在此处替换实现:
|
||||
// - 腾讯云:调用 /ocr/v3/BankCardOCR
|
||||
// - 阿里云:调用 /cardrecog/recognize_bank_card
|
||||
// - 自有服务:调用项目后端 OCR 接口
|
||||
// 注意:不要在此硬编码任何 SecretKey / AccessKey
|
||||
// ─────────────────────────────────────────────
|
||||
export type OCRResult = {
|
||||
success: boolean
|
||||
cardNo: string
|
||||
bankName: string // 识别出的银行名称(可能为空)
|
||||
errorMsg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择图片并识别银行卡
|
||||
* 当前为"前端完整流程 + 后端待接入"占位实现
|
||||
* @returns OCRResult
|
||||
*/
|
||||
export async function recognizeBankCardFromImage(): Promise<OCRResult> {
|
||||
return new Promise<OCRResult>((resolve) => {
|
||||
function handleSelectedImage(filePath: string): void {
|
||||
if (filePath === '') {
|
||||
resolve({ success: false, cardNo: '', bankName: '', errorMsg: '未选择图片' } as OCRResult)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[OCR] 已选择图片:', filePath, ',OCR 服务待接入')
|
||||
resolve({
|
||||
success: false,
|
||||
cardNo: '',
|
||||
bankName: '',
|
||||
errorMsg: 'OCR 服务待接入,请手动输入卡号'
|
||||
} as OCRResult)
|
||||
}
|
||||
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['camera', 'album'],
|
||||
success: (imgRes: ChooseImageSuccess): void => {
|
||||
const filePath = (imgRes.tempFilePaths as string[])[0]
|
||||
try {
|
||||
handleSelectedImage(filePath)
|
||||
} catch (e) {
|
||||
console.error('[OCR] 识别异常:', e)
|
||||
resolve({ success: false, cardNo: '', bankName: '', errorMsg: 'OCR 识别失败' } as OCRResult)
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
resolve({ success: false, cardNo: '', bankName: '', errorMsg: '取消选择' } as OCRResult)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 8. 短信验证码适配层
|
||||
// 【接入说明】项目后端接入短信服务后,替换此方法
|
||||
// ─────────────────────────────────────────────
|
||||
export type SmsResult = {
|
||||
success: boolean
|
||||
errorMsg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* 当前为占位实现,接入阿里云/腾讯云短信后替换此处
|
||||
* @param phone 11 位手机号
|
||||
*/
|
||||
export async function sendSmsCode(phone: string): Promise<SmsResult> {
|
||||
if (!isValidPhone(phone)) {
|
||||
return { success: false, errorMsg: '手机号格式不正确' } as SmsResult
|
||||
}
|
||||
|
||||
try {
|
||||
// ─── TODO: 接入真实短信服务 ────────────────────────────────
|
||||
// 示例(项目后端代理):
|
||||
// const res = await uni.request({
|
||||
// url: '/api/sms/send',
|
||||
// method: 'POST',
|
||||
// data: { phone, scene: 'bind_bank_card' }
|
||||
// })
|
||||
// if (res.data.code === 0) return { success: true, errorMsg: '' }
|
||||
// return { success: false, errorMsg: res.data.message }
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
// 占位实现:模拟发送成功,验证码请查看控制台
|
||||
console.log('[SMS] 占位发送验证码到:', phone, ',模拟验证码: 123456')
|
||||
return { success: true, errorMsg: '' } as SmsResult
|
||||
} catch (e) {
|
||||
console.error('[SMS] 发送异常:', e)
|
||||
return { success: false, errorMsg: '发送失败,请稍后重试' } as SmsResult
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
* 当前为占位实现:任何 6 位数字均视为通过
|
||||
* @param phone 手机号
|
||||
* @param code 用户输入的验证码
|
||||
*/
|
||||
export async function verifySmsCode(phone: string, code: string): Promise<SmsResult> {
|
||||
if (code.length !== 6) {
|
||||
return { success: false, errorMsg: '请输入 6 位验证码' } as SmsResult
|
||||
}
|
||||
|
||||
try {
|
||||
// ─── TODO: 接入真实验证逻辑 ────────────────────────────────
|
||||
// const res = await uni.request({ ... })
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
// 占位:校验通过
|
||||
console.log('[SMS] 占位验证通过, phone:', phone, 'code:', code)
|
||||
return { success: true, errorMsg: '' } as SmsResult
|
||||
} catch (e) {
|
||||
console.error('[SMS] 验证异常:', e)
|
||||
return { success: false, errorMsg: '验证失败,请重试' } as SmsResult
|
||||
}
|
||||
}
|
||||
87
utils/mock-pdd-waterfall-data.uts
Normal file
87
utils/mock-pdd-waterfall-data.uts
Normal file
@@ -0,0 +1,87 @@
|
||||
export type PddMockProduct = {
|
||||
id: string
|
||||
title: string
|
||||
image: string
|
||||
tags: string[]
|
||||
price: number
|
||||
salesText: string
|
||||
seedText: string
|
||||
}
|
||||
|
||||
const imagePool: string[] = [
|
||||
'/static/images/product/p1.jpg',
|
||||
'/static/images/product/p2.jpg',
|
||||
'/static/images/product/p3.jpg',
|
||||
'/static/images/product/p4.jpg',
|
||||
'/static/images/default-product.png',
|
||||
'/static/images/product/p1.jpg',
|
||||
'/static/images/product/p2.jpg',
|
||||
'/static/images/product/p3.jpg'
|
||||
]
|
||||
|
||||
const titlePool: string[] = [
|
||||
'老钱风针织短袖Polo衫显瘦百搭',
|
||||
'垂感阔腿牛仔裤女春夏高腰显腿长',
|
||||
'新疆吊干杏整箱新鲜脆甜当季水果',
|
||||
'便携充电宝22.5W快充小巧大容量',
|
||||
'冰丝凉感防晒外套轻薄透气户外通勤',
|
||||
'无线蓝牙耳机半入耳长续航降噪',
|
||||
'儿童速干T恤夏季吸汗透气套装',
|
||||
'加厚纯棉四件套亲肤柔软不易起球',
|
||||
'懒人沙发单人阳台小户型云朵坐垫',
|
||||
'大容量收纳箱家用衣物整理带滑轮',
|
||||
'原切牛排家庭装鲜嫩多汁煎烤皆宜',
|
||||
'法式轻奢玻璃水杯高颜值办公室耐热'
|
||||
]
|
||||
|
||||
const tagsPool: string[][] = [
|
||||
['billion_subsidy', 'delivery_48h'],
|
||||
['official_subsidy', 'pay_later'],
|
||||
['billion_subsidy'],
|
||||
['delivery_48h', 'pay_later'],
|
||||
['official_subsidy'],
|
||||
['billion_subsidy', 'official_subsidy']
|
||||
]
|
||||
|
||||
const salesPool: string[] = [
|
||||
'全店已售90万+',
|
||||
'全店已售32万+',
|
||||
'全店已售11万+',
|
||||
'全店已售5.6万+',
|
||||
'全店已售2900+'
|
||||
]
|
||||
|
||||
const seedPool: string[] = [
|
||||
'超10万人种草',
|
||||
'回头客都在买',
|
||||
'直播间同款热卖',
|
||||
'近2万人加购',
|
||||
'收藏量持续上涨'
|
||||
]
|
||||
|
||||
function buildMockProduct(index: number): PddMockProduct {
|
||||
const image = imagePool[index % imagePool.length]
|
||||
const title = titlePool[index % titlePool.length]
|
||||
const tags = tagsPool[index % tagsPool.length]
|
||||
const salesText = salesPool[index % salesPool.length]
|
||||
const seedText = seedPool[index % seedPool.length]
|
||||
const basePrice = 19.9 + (index % 7) * 13.3 + Math.floor(index / 3)
|
||||
|
||||
return {
|
||||
id: 'pdd-mock-' + index,
|
||||
title,
|
||||
image,
|
||||
tags,
|
||||
price: basePrice,
|
||||
salesText,
|
||||
seedText
|
||||
}
|
||||
}
|
||||
|
||||
export function createPddMockProducts(total: number = 80): PddMockProduct[] {
|
||||
const list: PddMockProduct[] = []
|
||||
for (let i = 0; i < total; i++) {
|
||||
list.push(buildMockProduct(i))
|
||||
}
|
||||
return list
|
||||
}
|
||||
205
utils/mockChannelData.uts
Normal file
205
utils/mockChannelData.uts
Normal file
@@ -0,0 +1,205 @@
|
||||
export type ChannelProduct = {
|
||||
id: string
|
||||
name: string
|
||||
shortName: string
|
||||
image: string
|
||||
price: number
|
||||
marketPrice: number
|
||||
tag: string
|
||||
}
|
||||
|
||||
export type MarketingChannel = {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
badge: string
|
||||
themeColor: string
|
||||
bgColor: string
|
||||
routeType: string
|
||||
layoutType: string
|
||||
products: ChannelProduct[]
|
||||
moreProducts: ChannelProduct[]
|
||||
}
|
||||
|
||||
export type SimpleCategoryChannel = {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
routeType: string
|
||||
icon: string
|
||||
coverImages: string[]
|
||||
categoryId: string
|
||||
}
|
||||
|
||||
const DEFAULT_CHANNEL_IMAGE = '/static/images/default.png'
|
||||
|
||||
function createChannelProduct(
|
||||
id: string,
|
||||
name: string,
|
||||
shortName: string,
|
||||
price: number,
|
||||
marketPrice: number,
|
||||
tag: string
|
||||
): ChannelProduct {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
shortName,
|
||||
image: DEFAULT_CHANNEL_IMAGE,
|
||||
price,
|
||||
marketPrice,
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
function cloneProducts(products: ChannelProduct[]): ChannelProduct[] {
|
||||
const result: ChannelProduct[] = []
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
const item = products[i]
|
||||
result.push({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
shortName: item.shortName,
|
||||
image: item.image,
|
||||
price: item.price,
|
||||
marketPrice: item.marketPrice,
|
||||
tag: item.tag
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function createMarketingChannel(
|
||||
id: string,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
badge: string,
|
||||
themeColor: string,
|
||||
bgColor: string,
|
||||
routeType: string,
|
||||
layoutType: string,
|
||||
products: ChannelProduct[],
|
||||
moreProducts: ChannelProduct[]
|
||||
): MarketingChannel {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
badge,
|
||||
themeColor,
|
||||
bgColor,
|
||||
routeType,
|
||||
layoutType,
|
||||
products: cloneProducts(products),
|
||||
moreProducts: cloneProducts(moreProducts)
|
||||
}
|
||||
}
|
||||
|
||||
function buildRecommendMarketingChannels(): MarketingChannel[] {
|
||||
const subsidySeed: ChannelProduct[] = [
|
||||
createChannelProduct('subsidy-1', '智能学习平板', '学习平板', 295, 499, '补贴价'),
|
||||
createChannelProduct('subsidy-2', '遥控赛车玩具', '遥控车', 174.3, 299, '补贴价')
|
||||
]
|
||||
const subsidyMore: ChannelProduct[] = [
|
||||
createChannelProduct('subsidy-3', '家用护眼台灯', '护眼台灯', 129, 199, '直降'),
|
||||
createChannelProduct('subsidy-4', '儿童积木礼盒', '积木礼盒', 89, 139, '补贴价'),
|
||||
createChannelProduct('subsidy-5', '便携筋膜枪', '筋膜枪', 239, 399, '官方补贴'),
|
||||
createChannelProduct('subsidy-6', '智能空气炸锅', '空气炸锅', 269, 459, '限时补贴')
|
||||
]
|
||||
|
||||
const qualitySeed: ChannelProduct[] = [
|
||||
createChannelProduct('quality-1', '超市配送日用品', '超市配送', 19.9, 29.9, '实惠'),
|
||||
createChannelProduct('quality-2', '美食团购套餐', '美食团购', 39.9, 59.9, '团购')
|
||||
]
|
||||
const qualityMore: ChannelProduct[] = [
|
||||
createChannelProduct('quality-3', '健康轻食组合', '轻食组合', 28.8, 39.8, '精选'),
|
||||
createChannelProduct('quality-4', '家居香氛礼盒', '香氛礼盒', 59, 89, '品质价'),
|
||||
createChannelProduct('quality-5', '鲜果早餐套餐', '鲜果套餐', 26.8, 36.8, '人气'),
|
||||
createChannelProduct('quality-6', '净味洗护套装', '洗护套装', 49.9, 69.9, '好评')
|
||||
]
|
||||
|
||||
const cheapSeed: ChannelProduct[] = [
|
||||
createChannelProduct('cheap-1', '夏季西瓜鲜果', '新鲜西瓜', 9.8, 19.9, '9.9包邮'),
|
||||
createChannelProduct('cheap-2', '厨房小工具套装', '厨房工具', 10.8, 18.8, '特价')
|
||||
]
|
||||
const cheapMore: ChannelProduct[] = [
|
||||
createChannelProduct('cheap-3', '旅行压缩毛巾', '压缩毛巾', 9.9, 15.9, '包邮'),
|
||||
createChannelProduct('cheap-4', '桌面收纳盒', '收纳盒', 9.9, 16.8, '限量'),
|
||||
createChannelProduct('cheap-5', '抹布海绵组合', '清洁抹布', 8.8, 12.8, '超省'),
|
||||
createChannelProduct('cheap-6', '便携数据线', '数据线', 9.9, 14.9, '补贴')
|
||||
]
|
||||
|
||||
const liveSeed: ChannelProduct[] = [
|
||||
createChannelProduct('live-1', '智能风扇套装', '智能风扇', 1099, 1399, '直播价'),
|
||||
createChannelProduct('live-2', '智能门锁套装', '智能门锁', 2699, 3299, '直播价')
|
||||
]
|
||||
const liveMore: ChannelProduct[] = [
|
||||
createChannelProduct('live-3', '无线吸尘器', '无线吸尘器', 699, 899, '直播间'),
|
||||
createChannelProduct('live-4', '按摩椅轻享版', '按摩椅', 1999, 2599, '限时抢'),
|
||||
createChannelProduct('live-5', '智能净水器', '净水器', 1599, 1999, '主播推荐'),
|
||||
createChannelProduct('live-6', '轻薄投影仪', '投影仪', 1399, 1799, '专享价')
|
||||
]
|
||||
|
||||
return [
|
||||
createMarketingChannel('subsidy', '百亿补贴', '大牌直降补贴价', '国家补贴', '#e02e24', '#fff6f4', 'subsidy', 'recommend-large', subsidySeed, subsidyMore),
|
||||
createMarketingChannel('quality-life', '品质生活', '本地好物精选', '品质严选', '#16a34a', '#f3fff7', 'quality', 'recommend-large', qualitySeed, qualityMore),
|
||||
createMarketingChannel('cheap-mail', '9.9包邮', '低价好物随心买', '低价包邮', '#ff4d00', '#fff8ed', 'cheap', 'recommend-large', cheapSeed, cheapMore),
|
||||
createMarketingChannel('live-low-price', '直播低价', '直播间好价精选', '直播好价', '#e11d48', '#fff1f5', 'live', 'recommend-large', liveSeed, liveMore)
|
||||
]
|
||||
}
|
||||
|
||||
function buildCategoryRankChannel(categoryId: string): MarketingChannel {
|
||||
const seedProducts: ChannelProduct[] = [
|
||||
createChannelProduct(categoryId + '-rank-1', '热销榜单精选好物', '榜单好物', 69.9, 99.9, '热销'),
|
||||
createChannelProduct(categoryId + '-rank-2', '高回购口碑单品', '口碑单品', 49.9, 79.9, '回购')
|
||||
]
|
||||
const moreProducts: ChannelProduct[] = [
|
||||
createChannelProduct(categoryId + '-rank-3', '每日热度上升款', '热度上升', 59.9, 89.9, '榜单'),
|
||||
createChannelProduct(categoryId + '-rank-4', '用户收藏热门款', '收藏热门', 45.9, 69.9, '精选'),
|
||||
createChannelProduct(categoryId + '-rank-5', '爆款囤货套装', '囤货套装', 79.9, 109.9, '热卖'),
|
||||
createChannelProduct(categoryId + '-rank-6', '销量冠军单品', '销量冠军', 99.9, 139.9, '冠军')
|
||||
]
|
||||
return createMarketingChannel('rank', '排行榜', '每日更新', '热销榜单', '#e02e24', '#fff6f4', 'rank', 'detail-list', seedProducts, moreProducts)
|
||||
}
|
||||
|
||||
function buildCategoryQualityChannel(categoryId: string): MarketingChannel {
|
||||
const seedProducts: ChannelProduct[] = [
|
||||
createChannelProduct(categoryId + '-quality-1', '品质优选套装组合', '精选套装', 88, 129, '严选'),
|
||||
createChannelProduct(categoryId + '-quality-2', '高分口碑精品单品', '高分精品', 56, 89, '口碑')
|
||||
]
|
||||
const moreProducts: ChannelProduct[] = [
|
||||
createChannelProduct(categoryId + '-quality-3', '质价比家用好物', '家用好物', 66, 99, '品质'),
|
||||
createChannelProduct(categoryId + '-quality-4', '长期复购推荐款', '复购推荐', 45, 69, '精选'),
|
||||
createChannelProduct(categoryId + '-quality-5', '舒适实用组合装', '组合装', 105, 149, '推荐'),
|
||||
createChannelProduct(categoryId + '-quality-6', '高满意度口碑款', '高满意度', 39.9, 59.9, '好评')
|
||||
]
|
||||
return createMarketingChannel('quality', '品质优选', '精选好货', '品质严选', '#16a34a', '#f3fff7', 'quality', 'detail-list', seedProducts, moreProducts)
|
||||
}
|
||||
|
||||
export function getRecommendMarketingChannels(): MarketingChannel[] {
|
||||
return buildRecommendMarketingChannels()
|
||||
}
|
||||
|
||||
export function getChannelDetailData(channelId: string, routeType: string, categoryId: string): MarketingChannel | null {
|
||||
if (categoryId === 'recommend') {
|
||||
const channels = buildRecommendMarketingChannels()
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const channel = channels[i]
|
||||
if (channel.id === channelId || channel.routeType === routeType) {
|
||||
return channel
|
||||
}
|
||||
}
|
||||
if (channels.length > 0) {
|
||||
return channels[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (routeType === 'rank') {
|
||||
return buildCategoryRankChannel(categoryId)
|
||||
}
|
||||
if (routeType === 'quality') {
|
||||
return buildCategoryQualityChannel(categoryId)
|
||||
}
|
||||
return buildCategoryRankChannel(categoryId)
|
||||
}
|
||||
@@ -6,10 +6,7 @@ export { supa }
|
||||
import type { OrderOptions } from '@/components/supadb/aksupa.uts'
|
||||
|
||||
const OLD_URL = '192.168.1.61:18000'
|
||||
// const NEW_URL = '119.146.131.237:9126'
|
||||
|
||||
// 医疗项目 Supabase 实例地址(与 ak/config.uts 中的 SUPA_URL 保持一致,去掉 http:// 前缀)
|
||||
const NEW_URL = '119.146.131.237:9127'
|
||||
const NEW_URL = '119.146.131.237:9126'
|
||||
|
||||
function fixImageUrl(url: string | null): string {
|
||||
if (url == null) return ''
|
||||
@@ -424,6 +421,16 @@ export type ShopOrderParams = {
|
||||
discountAmount: number
|
||||
}
|
||||
|
||||
function emptyProductPage(page: number, limit: number): PaginatedResponse<Product> {
|
||||
return {
|
||||
data: [] as Product[],
|
||||
total: 0,
|
||||
page,
|
||||
limit,
|
||||
hasmore: false
|
||||
}
|
||||
}
|
||||
|
||||
export type ShopOrderResponse = {
|
||||
success: boolean
|
||||
orderIds: string[]
|
||||
@@ -1730,25 +1737,26 @@ class SupabaseService {
|
||||
}
|
||||
|
||||
// 获取按销量排序的商品(所有商品,不限制 is_hot)
|
||||
async getProductsBySales(limit: number = 10): Promise<Product[]> {
|
||||
async getProductsBySales(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Product>> {
|
||||
try {
|
||||
console.log('[getProductsBySales] 开始获取销量排序商品...')
|
||||
const response = await supa
|
||||
.from('ml_products_detail_view')
|
||||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot')
|
||||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot', { count: 'exact' })
|
||||
.eq('status', '1')
|
||||
.order('sale_count', { ascending: false })
|
||||
.page(page)
|
||||
.limit(limit)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取销量排序商品失败:', response.error)
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
|
||||
const rawData = response.data
|
||||
if (rawData == null) {
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
|
||||
const products: Product[] = []
|
||||
@@ -1758,32 +1766,39 @@ class SupabaseService {
|
||||
products.push(parseProductFromRaw(item))
|
||||
}
|
||||
console.log('[getProductsBySales] 返回商品数:', products.length)
|
||||
return products
|
||||
return {
|
||||
data: products,
|
||||
total: response.total ?? products.length,
|
||||
page,
|
||||
limit,
|
||||
hasmore: response.hasmore ?? false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取销量排序商品异常:', error)
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取按价格排序的商品(升序:从低到高)
|
||||
async getProductsByPrice(limit: number = 10, ascending: boolean = true): Promise<Product[]> {
|
||||
async getProductsByPrice(page: number = 1, limit: number = 10, ascending: boolean = true): Promise<PaginatedResponse<Product>> {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_products_detail_view')
|
||||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot')
|
||||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot', { count: 'exact' })
|
||||
.eq('status', '1') // 在数据库层面过滤
|
||||
.order('base_price', { ascending })
|
||||
.page(page)
|
||||
.limit(limit)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取价格排序商品失败:', response.error)
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
|
||||
const rawData = response.data
|
||||
if (rawData == null) {
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
|
||||
const products: Product[] = []
|
||||
@@ -1792,33 +1807,40 @@ class SupabaseService {
|
||||
const item = rawList[i]
|
||||
products.push(parseProductFromRaw(item))
|
||||
}
|
||||
return products
|
||||
return {
|
||||
data: products,
|
||||
total: response.total ?? products.length,
|
||||
page,
|
||||
limit,
|
||||
hasmore: response.hasmore ?? false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取价格排序商品异常:', error)
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取新品(按创建时间排序,最新的在前)
|
||||
async getProductsByNewest(limit: number = 10): Promise<Product[]> {
|
||||
async getProductsByNewest(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Product>> {
|
||||
try {
|
||||
console.log('[getProductsByNewest] 开始获取新品...')
|
||||
const response = await supa
|
||||
.from('ml_products_detail_view')
|
||||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot')
|
||||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot', { count: 'exact' })
|
||||
.eq('status', '1')
|
||||
.order('created_at', { ascending: false })
|
||||
.page(page)
|
||||
.limit(limit * 5)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取新品失败:', response.error)
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
|
||||
const rawData = response.data
|
||||
if (rawData == null) {
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
|
||||
const products: Product[] = []
|
||||
@@ -1864,10 +1886,17 @@ class SupabaseService {
|
||||
}
|
||||
|
||||
console.log('[getProductsByNewest] 返回商品数:', products.length)
|
||||
return products
|
||||
const pageData = products.slice(0, limit)
|
||||
return {
|
||||
data: pageData,
|
||||
total: response.total ?? products.length,
|
||||
page,
|
||||
limit,
|
||||
hasmore: products.length > limit || (response.hasmore ?? false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取新品异常:', error)
|
||||
return []
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5821,12 +5850,13 @@ class SupabaseService {
|
||||
}
|
||||
|
||||
// 智能推荐:综合用户搜索历史、浏览历史、热销商品
|
||||
async getSmartRecommendations(limit: number = 10): Promise<Product[]> {
|
||||
async getSmartRecommendations(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Product>> {
|
||||
try {
|
||||
console.log('[getSmartRecommendations] 开始获取智能推荐...')
|
||||
|
||||
const products: Product[] = []
|
||||
const addedIds = new Set<string>()
|
||||
const requiredCount = page * limit + 1
|
||||
|
||||
// 1. 根据用户搜索历史推荐商品(权重最高)
|
||||
const searchHistory = await this.getUserSearchHistory(5)
|
||||
@@ -5834,7 +5864,7 @@ class SupabaseService {
|
||||
|
||||
if (searchHistory.length > 0) {
|
||||
// 根据搜索关键词查找商品
|
||||
const keywordProducts = await this.searchProductsByKeywords(searchHistory, limit)
|
||||
const keywordProducts = await this.searchProductsByKeywords(searchHistory, requiredCount)
|
||||
for (let i = 0; i < keywordProducts.length; i++) {
|
||||
const prod = keywordProducts[i]
|
||||
if (!addedIds.has(prod.id)) {
|
||||
@@ -5845,53 +5875,63 @@ class SupabaseService {
|
||||
}
|
||||
|
||||
// 2. 根据用户浏览历史推荐相似分类商品
|
||||
if (products.length < limit) {
|
||||
if (products.length < requiredCount) {
|
||||
const browseCategories = await this.getUserBrowseCategories(3)
|
||||
console.log('[getSmartRecommendations] 用户浏览分类:', browseCategories)
|
||||
|
||||
if (browseCategories.length > 0) {
|
||||
const categoryProducts = await this.getProductsByCategories(browseCategories, limit - products.length)
|
||||
const categoryProducts = await this.getProductsByCategories(browseCategories, requiredCount - products.length)
|
||||
for (let i = 0; i < categoryProducts.length; i++) {
|
||||
const prod = categoryProducts[i]
|
||||
if (!addedIds.has(prod.id)) {
|
||||
products.push(prod)
|
||||
addedIds.add(prod.id)
|
||||
if (products.length >= requiredCount) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 补充热销商品
|
||||
if (products.length < limit) {
|
||||
const hotProducts = await this.getHotProducts(limit - products.length + 5)
|
||||
if (products.length < requiredCount) {
|
||||
const hotProducts = await this.getHotProducts(requiredCount - products.length + 5)
|
||||
for (let i = 0; i < hotProducts.length; i++) {
|
||||
const prod = hotProducts[i]
|
||||
if (!addedIds.has(prod.id)) {
|
||||
products.push(prod)
|
||||
addedIds.add(prod.id)
|
||||
if (products.length >= limit) break
|
||||
if (products.length >= requiredCount) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 如果还不够,用普通商品补充
|
||||
if (products.length < limit) {
|
||||
const moreProducts = await this.getProductsByPrice(limit - products.length + 5, false)
|
||||
for (let i = 0; i < moreProducts.length; i++) {
|
||||
const prod = moreProducts[i]
|
||||
if (products.length < requiredCount) {
|
||||
const moreProducts = await this.getProductsByPrice(1, requiredCount - products.length + 5, false)
|
||||
for (let i = 0; i < moreProducts.data.length; i++) {
|
||||
const prod = moreProducts.data[i]
|
||||
if (!addedIds.has(prod.id)) {
|
||||
products.push(prod)
|
||||
addedIds.add(prod.id)
|
||||
if (products.length >= limit) break
|
||||
if (products.length >= requiredCount) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[getSmartRecommendations] 返回商品数量:', products.length)
|
||||
return products.slice(0, limit)
|
||||
const startIndex = (page - 1) * limit
|
||||
const endIndex = startIndex + limit
|
||||
const pageData = products.slice(startIndex, endIndex)
|
||||
console.log('[getSmartRecommendations] 返回商品数量:', pageData.length)
|
||||
return {
|
||||
data: pageData,
|
||||
total: products.length,
|
||||
page,
|
||||
limit,
|
||||
hasmore: products.length > endIndex
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取智能推荐失败:', e)
|
||||
return [] as Product[]
|
||||
return emptyProductPage(page, limit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user