feat(order): 实现优惠券智能推荐与自动应用功能
新增优惠券智能推荐逻辑,在订单确认页面中根据商品总价自动筛选并推荐最优优惠券。支持以下特性:- 自动识别可用优惠券并排序 - 未选择优惠券时自动应用最优券- 已选优惠券时提示是否存在更优选项 - 支付前再次提醒用户使用可用优惠券 - UI 上显示可用优惠券数量及选择状态 同时更新开发环境 API 地址为本地调试地址。
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
export const ENV_CONFIG = {
|
export const ENV_CONFIG = {
|
||||||
// 开发环境
|
// 开发环境
|
||||||
development: {
|
development: {
|
||||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||||
APP_NAME: '开发环境',
|
APP_NAME: '开发环境',
|
||||||
DEBUG: 'true',
|
DEBUG: 'true',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ const OrderConfirm = () => {
|
|||||||
if (availableCoupons.length > 0) {
|
if (availableCoupons.length > 0) {
|
||||||
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
|
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
|
||||||
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
|
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
|
||||||
|
const usableCoupons = filterUsableCoupons(sortedCoupons, newTotal)
|
||||||
setAvailableCoupons(sortedCoupons)
|
setAvailableCoupons(sortedCoupons)
|
||||||
|
|
||||||
// 检查当前选中的优惠券是否还可用
|
// 检查当前选中的优惠券是否还可用
|
||||||
@@ -115,6 +116,56 @@ const OrderConfirm = () => {
|
|||||||
title: '当前优惠券不满足使用条件,已自动取消',
|
title: '当前优惠券不满足使用条件,已自动取消',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 🎯 自动推荐新的最优优惠券
|
||||||
|
if (usableCoupons.length > 0) {
|
||||||
|
const bestCoupon = usableCoupons[0]
|
||||||
|
const discount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||||
|
|
||||||
|
if (discount > 0) {
|
||||||
|
setSelectedCoupon(bestCoupon)
|
||||||
|
Taro.showToast({
|
||||||
|
title: `已为您重新推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!selectedCoupon && usableCoupons.length > 0) {
|
||||||
|
// 🔔 如果没有选中优惠券但有可用的,推荐最优的
|
||||||
|
const bestCoupon = usableCoupons[0]
|
||||||
|
const discount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||||
|
|
||||||
|
if (discount > 0) {
|
||||||
|
setSelectedCoupon(bestCoupon)
|
||||||
|
Taro.showToast({
|
||||||
|
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (selectedCoupon && usableCoupons.length > 0) {
|
||||||
|
// 🔍 检查是否有更好的优惠券
|
||||||
|
const bestCoupon = usableCoupons[0]
|
||||||
|
const currentDiscount = calculateCouponDiscount(selectedCoupon, newTotal)
|
||||||
|
const bestDiscount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||||
|
|
||||||
|
// 如果有更好的优惠券(优惠超过0.01元)
|
||||||
|
if (bestDiscount > currentDiscount + 0.01 && bestCoupon.id !== selectedCoupon.id) {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '发现更优惠的优惠券',
|
||||||
|
content: `有更好的优惠券可用,额外节省¥${(bestDiscount - currentDiscount).toFixed(2)},是否更换?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
setSelectedCoupon(bestCoupon)
|
||||||
|
Taro.showToast({
|
||||||
|
title: '优惠券已更换',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,13 +216,38 @@ const OrderConfirm = () => {
|
|||||||
// 按优惠金额排序
|
// 按优惠金额排序
|
||||||
const total = getGoodsTotal()
|
const total = getGoodsTotal()
|
||||||
const sortedCoupons = sortCoupons(transformedCoupons, total)
|
const sortedCoupons = sortCoupons(transformedCoupons, total)
|
||||||
|
const usableCoupons = filterUsableCoupons(sortedCoupons, total)
|
||||||
|
|
||||||
setAvailableCoupons(sortedCoupons)
|
setAvailableCoupons(sortedCoupons)
|
||||||
|
|
||||||
|
// 🎯 智能推荐:自动应用最优惠的可用优惠券
|
||||||
|
if (usableCoupons.length > 0 && !selectedCoupon) {
|
||||||
|
const bestCoupon = usableCoupons[0] // 已经按优惠金额排序,第一个就是最优的
|
||||||
|
const discount = calculateCouponDiscount(bestCoupon, total)
|
||||||
|
|
||||||
|
if (discount > 0) {
|
||||||
|
setSelectedCoupon(bestCoupon)
|
||||||
|
|
||||||
|
// 显示智能推荐提示
|
||||||
|
Taro.showToast({
|
||||||
|
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔔 优惠券提示:如果有可用优惠券,显示提示
|
||||||
|
if (usableCoupons.length > 0) {
|
||||||
|
console.log(`发现${usableCoupons.length}张可用优惠券,已为您推荐最优惠券`)
|
||||||
|
}
|
||||||
|
|
||||||
console.log('加载优惠券成功:', {
|
console.log('加载优惠券成功:', {
|
||||||
originalData: res,
|
originalData: res,
|
||||||
transformedData: transformedCoupons,
|
transformedData: transformedCoupons,
|
||||||
sortedData: sortedCoupons
|
sortedData: sortedCoupons,
|
||||||
|
usableCoupons: usableCoupons,
|
||||||
|
recommendedCoupon: usableCoupons[0] || null
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setAvailableCoupons([])
|
setAvailableCoupons([])
|
||||||
@@ -233,6 +309,35 @@ const OrderConfirm = () => {
|
|||||||
})
|
})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 🔔 支付前最后一次检查:提醒用户是否有可用优惠券
|
||||||
|
const total = getGoodsTotal()
|
||||||
|
const usableCoupons = filterUsableCoupons(availableCoupons, total)
|
||||||
|
|
||||||
|
if (usableCoupons.length > 0) {
|
||||||
|
const bestCoupon = usableCoupons[0]
|
||||||
|
const discount = calculateCouponDiscount(bestCoupon, total)
|
||||||
|
|
||||||
|
if (discount > 0) {
|
||||||
|
// 用模态框提醒用户
|
||||||
|
const confirmResult = await new Promise<boolean>((resolve) => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '发现可用优惠券',
|
||||||
|
content: `您有优惠券可使用,可省¥${discount.toFixed(2)},是否使用?`,
|
||||||
|
success: (res) => resolve(res.confirm),
|
||||||
|
fail: () => resolve(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (confirmResult) {
|
||||||
|
setSelectedCoupon(bestCoupon)
|
||||||
|
// 使用优惠券后重新计算价格,然后继续支付
|
||||||
|
// 注意:这里不直接return,让代码继续执行支付逻辑
|
||||||
|
} else {
|
||||||
|
// 用户选择不使用优惠券,继续支付
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建订单数据
|
// 构建订单数据
|
||||||
@@ -474,7 +579,31 @@ const OrderConfirm = () => {
|
|||||||
<View className={'text-red-500 text-sm mr-1'}>
|
<View className={'text-red-500 text-sm mr-1'}>
|
||||||
{selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'}
|
{selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'}
|
||||||
</View>
|
</View>
|
||||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
{(() => {
|
||||||
|
const usableCoupons = filterUsableCoupons(availableCoupons, getGoodsTotal())
|
||||||
|
if (usableCoupons.length > 0 && !selectedCoupon) {
|
||||||
|
return (
|
||||||
|
<View className={'flex items-center'}>
|
||||||
|
<View className={'bg-red-500 text-white text-xs px-2 py-1 rounded mr-2'}>
|
||||||
|
{usableCoupons.length}张可用
|
||||||
|
</View>
|
||||||
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
} else if (usableCoupons.length > 0) {
|
||||||
|
return (
|
||||||
|
<View className={'flex items-center'}>
|
||||||
|
<View className={'bg-green-500 text-white text-xs px-2 py-1 rounded mr-2'}>
|
||||||
|
已选择
|
||||||
|
</View>
|
||||||
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return <ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
onClick={() => setCouponVisible(true)}
|
onClick={() => setCouponVisible(true)}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps =>
|
|||||||
const getTheme = (type?: number): CouponCardProps['theme'] => {
|
const getTheme = (type?: number): CouponCardProps['theme'] => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 10: return 'red' // 满减券-红色
|
case 10: return 'red' // 满减券-红色
|
||||||
case 20: return 'orange' // 折扣券-橙色
|
case 20: return 'orange' // 折扣券-橙色
|
||||||
case 30: return 'green' // 免费券-绿色
|
case 30: return 'green' // 免费券-绿色
|
||||||
default: return 'blue'
|
default: return 'blue'
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps =>
|
|||||||
* 计算优惠券折扣金额
|
* 计算优惠券折扣金额
|
||||||
*/
|
*/
|
||||||
export const calculateCouponDiscount = (
|
export const calculateCouponDiscount = (
|
||||||
coupon: CouponCardProps,
|
coupon: CouponCardProps,
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
): number => {
|
): number => {
|
||||||
// 检查是否满足使用条件
|
// 检查是否满足使用条件
|
||||||
@@ -82,7 +82,7 @@ export const calculateCouponDiscount = (
|
|||||||
* 检查优惠券是否可用
|
* 检查优惠券是否可用
|
||||||
*/
|
*/
|
||||||
export const isCouponUsable = (
|
export const isCouponUsable = (
|
||||||
coupon: CouponCardProps,
|
coupon: CouponCardProps,
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// 状态检查
|
// 状态检查
|
||||||
@@ -102,13 +102,13 @@ export const isCouponUsable = (
|
|||||||
* 获取优惠券不可用原因
|
* 获取优惠券不可用原因
|
||||||
*/
|
*/
|
||||||
export const getCouponUnusableReason = (
|
export const getCouponUnusableReason = (
|
||||||
coupon: CouponCardProps,
|
coupon: CouponCardProps,
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
): string => {
|
): string => {
|
||||||
if (coupon.status === 1) {
|
if (coupon.status === 1) {
|
||||||
return '优惠券已使用'
|
return '优惠券已使用'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coupon.status === 2) {
|
if (coupon.status === 2) {
|
||||||
return '优惠券已过期'
|
return '优惠券已过期'
|
||||||
}
|
}
|
||||||
@@ -151,30 +151,30 @@ export const formatCouponTitle = (coupon: CouponCardProps): string => {
|
|||||||
* 按照优惠金额从大到小排序,同等优惠金额按过期时间排序
|
* 按照优惠金额从大到小排序,同等优惠金额按过期时间排序
|
||||||
*/
|
*/
|
||||||
export const sortCoupons = (
|
export const sortCoupons = (
|
||||||
coupons: CouponCardProps[],
|
coupons: CouponCardProps[],
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
): CouponCardProps[] => {
|
): CouponCardProps[] => {
|
||||||
return [...coupons].sort((a, b) => {
|
return [...coupons].sort((a, b) => {
|
||||||
// 先按可用性排序
|
// 先按可用性排序
|
||||||
const aUsable = isCouponUsable(a, totalAmount)
|
const aUsable = isCouponUsable(a, totalAmount)
|
||||||
const bUsable = isCouponUsable(b, totalAmount)
|
const bUsable = isCouponUsable(b, totalAmount)
|
||||||
|
|
||||||
if (aUsable && !bUsable) return -1
|
if (aUsable && !bUsable) return -1
|
||||||
if (!aUsable && bUsable) return 1
|
if (!aUsable && bUsable) return 1
|
||||||
|
|
||||||
// 都可用或都不可用时,按优惠金额排序
|
// 都可用或都不可用时,按优惠金额排序
|
||||||
const aDiscount = calculateCouponDiscount(a, totalAmount)
|
const aDiscount = calculateCouponDiscount(a, totalAmount)
|
||||||
const bDiscount = calculateCouponDiscount(b, totalAmount)
|
const bDiscount = calculateCouponDiscount(b, totalAmount)
|
||||||
|
|
||||||
if (aDiscount !== bDiscount) {
|
if (aDiscount !== bDiscount) {
|
||||||
return bDiscount - aDiscount // 优惠金额大的在前
|
return bDiscount - aDiscount // 优惠金额大的在前
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优惠金额相同时,按过期时间排序(即将过期的在前)
|
// 优惠金额相同时,按过期时间排序(即将过期的在前)
|
||||||
if (a.endTime && b.endTime) {
|
if (a.endTime && b.endTime) {
|
||||||
return new Date(a.endTime).getTime() - new Date(b.endTime).getTime()
|
return new Date(a.endTime).getTime() - new Date(b.endTime).getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ export const sortCoupons = (
|
|||||||
* 过滤可用优惠券
|
* 过滤可用优惠券
|
||||||
*/
|
*/
|
||||||
export const filterUsableCoupons = (
|
export const filterUsableCoupons = (
|
||||||
coupons: CouponCardProps[],
|
coupons: CouponCardProps[],
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
): CouponCardProps[] => {
|
): CouponCardProps[] => {
|
||||||
return coupons.filter(coupon => isCouponUsable(coupon, totalAmount))
|
return coupons.filter(coupon => isCouponUsable(coupon, totalAmount))
|
||||||
@@ -193,7 +193,7 @@ export const filterUsableCoupons = (
|
|||||||
* 过滤不可用优惠券
|
* 过滤不可用优惠券
|
||||||
*/
|
*/
|
||||||
export const filterUnusableCoupons = (
|
export const filterUnusableCoupons = (
|
||||||
coupons: CouponCardProps[],
|
coupons: CouponCardProps[],
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
): CouponCardProps[] => {
|
): CouponCardProps[] => {
|
||||||
return coupons.filter(coupon => !isCouponUsable(coupon, totalAmount))
|
return coupons.filter(coupon => !isCouponUsable(coupon, totalAmount))
|
||||||
|
|||||||
Reference in New Issue
Block a user