From 74fd855d336e6958b50a2f019aadadacf357d4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B1=BC=E5=BC=80=E5=8F=91?= Date: Mon, 18 May 2026 15:26:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(points):=20=E5=85=85=E5=80=BC=E6=A1=A3?= =?UTF-8?q?=E4=BD=8D=E6=B7=BB=E5=8A=A0=E7=A7=AF=E5=88=86=E6=9C=89=E6=95=88?= =?UTF-8?q?=E6=9C=9F=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config/points-config.yaml: 每个档位添加 validity_days(7/30/90/180/365/0) - points.py: 支付回调和主动查询补单时根据档位配置设置 batch_expired_at - RechargeModal: 卡片展示有效期(永久有效 / N 天内有效) --- python-api/app/api/v1/points.py | 31 +++++++++++++++++++ python-api/config/points-config.yaml | 12 +++---- tauri-app/src/api/modules/points.ts | 1 + .../RechargeModal/RechargeModal.css | 16 ++++++++++ .../RechargeModal/RechargeModal.tsx | 5 ++- 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/python-api/app/api/v1/points.py b/python-api/app/api/v1/points.py index 9641888..b2c7208 100644 --- a/python-api/app/api/v1/points.py +++ b/python-api/app/api/v1/points.py @@ -329,6 +329,21 @@ async def handle_wxpay_notify( await db.commit() # 提交 notify_raw 等记录 return _wx_response() + # 根据档位确定积分有效期 + validity_days = point_service.EXPIRATION_DAYS + for opt in point_service.get_recharge_options(): + if opt.get("points") == order.points and opt.get("price") == order.amount_rmb: + validity_days = opt.get("validity_days", point_service.EXPIRATION_DAYS) + break + + if validity_days is not None and validity_days > 0: + batch_expired_at = datetime.now(UTC) + timedelta(days=validity_days) + elif validity_days == 0: + # 永久有效 + batch_expired_at = datetime.now(UTC) + timedelta(days=36500) + else: + batch_expired_at = None + # 更新订单状态并充值积分(同一事务) try: order.status = "paid" @@ -342,6 +357,7 @@ async def handle_wxpay_notify( source="wxpay", description=f"微信支付充值 {order.points} 积分", order_id=order.id, + batch_expired_at=batch_expired_at, ) await db.commit() @@ -397,6 +413,20 @@ async def query_recharge_status( order.paid_at = datetime.now(UTC) order.wx_order_no = wx_result.get("transaction_id") + # 根据档位确定积分有效期 + validity_days = point_service.EXPIRATION_DAYS + for opt in point_service.get_recharge_options(): + if opt.get("points") == order.points and opt.get("price") == order.amount_rmb: + validity_days = opt.get("validity_days", point_service.EXPIRATION_DAYS) + break + + if validity_days is not None and validity_days > 0: + batch_expired_at = datetime.now(UTC) + timedelta(days=validity_days) + elif validity_days == 0: + batch_expired_at = datetime.now(UTC) + timedelta(days=36500) + else: + batch_expired_at = None + await point_service.recharge( db, user_id=order.user_id, @@ -404,6 +434,7 @@ async def query_recharge_status( source="wxpay", description=f"微信支付充值 {order.points} 积分(主动查询补单)", order_id=order.id, + batch_expired_at=batch_expired_at, ) logger.info( f"[Points] 订单 {order.out_trade_no} 通过主动查询确认支付成功," diff --git a/python-api/config/points-config.yaml b/python-api/config/points-config.yaml index b983c0a..f0935be 100644 --- a/python-api/config/points-config.yaml +++ b/python-api/config/points-config.yaml @@ -77,12 +77,12 @@ duration_based_costs: # 支持积分赠送:points 为实际到账积分数,amount_rmb 为支付金额(分)。 # label 为空时不显示标签角标。 recharge_options: - - { price: 1, points: 100, label: "测试" } - - { price: 500, points: 100, label: "入门" } - - { price: 1000, points: 220, label: "热销" } - - { price: 3000, points: 650, label: "推荐" } - - { price: 5000, points: 1200, label: "超值" } - - { price: 10000, points: 2500, label: "尊享" } + - { price: 1, points: 100, label: "测试", validity_days: 7 } + - { price: 500, points: 100, label: "入门", validity_days: 30 } + - { price: 1000, points: 220, label: "热销", validity_days: 90 } + - { price: 3000, points: 650, label: "推荐", validity_days: 180 } + - { price: 5000, points: 1200, label: "超值", validity_days: 365 } + - { price: 10000, points: 2500, label: "尊享", validity_days: 0 } # ── 免费业务(不扣积分)─────────────────────────────── diff --git a/tauri-app/src/api/modules/points.ts b/tauri-app/src/api/modules/points.ts index 98f3282..0a8cb79 100644 --- a/tauri-app/src/api/modules/points.ts +++ b/tauri-app/src/api/modules/points.ts @@ -20,6 +20,7 @@ export interface RechargeOption { price: number; // 人民币(分) points: number; // 到账积分数 label: string; // 标签,空字符串不显示 + validityDays: number; // 积分有效期(天),0 表示永久有效 } export interface RechargeOrder { diff --git a/tauri-app/src/components/RechargeModal/RechargeModal.css b/tauri-app/src/components/RechargeModal/RechargeModal.css index 0b24d38..1b6072d 100644 --- a/tauri-app/src/components/RechargeModal/RechargeModal.css +++ b/tauri-app/src/components/RechargeModal/RechargeModal.css @@ -80,6 +80,15 @@ animation: recharge-skeleton-pulse 1.5s ease-in-out infinite; } +.recharge-amount-card.skeleton .skeleton-validity { + width: 60px; + height: 12px; + background: var(--border-light); + border-radius: 4px; + margin-top: 4px; + animation: recharge-skeleton-pulse 1.5s ease-in-out infinite; +} + @keyframes recharge-skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } @@ -97,6 +106,13 @@ margin-top: 4px; } +.recharge-amount-card .validity { + font-size: 11px; + color: #ff6b6b; + margin-top: 2px; + font-weight: 500; +} + .recharge-amount-card .tag { position: absolute; top: -1px; diff --git a/tauri-app/src/components/RechargeModal/RechargeModal.tsx b/tauri-app/src/components/RechargeModal/RechargeModal.tsx index d4d04e1..e3168ce 100644 --- a/tauri-app/src/components/RechargeModal/RechargeModal.tsx +++ b/tauri-app/src/components/RechargeModal/RechargeModal.tsx @@ -25,6 +25,7 @@ interface AmountOption { price: number; // 人民币(分) points: number; label?: string; + validityDays: number; // 积分有效期(天),0 表示永久有效 } const POLLING_INTERVAL = 5000; // 5 秒(温和轮询,减少请求) @@ -97,7 +98,7 @@ export default function RechargeModal({ setIsLoadingOptions(true); pointsApi.getRechargeOptions() .then(data => { - setOptions(data.map(o => ({ price: o.price, points: o.points, label: o.label || undefined }))); + setOptions(data.map(o => ({ price: o.price, points: o.points, label: o.label || undefined, validityDays: o.validityDays ?? 180 }))); setIsLoadingOptions(false); }) .catch(e => { @@ -263,6 +264,7 @@ export default function RechargeModal({
+
)) : options.map(option => ( @@ -274,6 +276,7 @@ export default function RechargeModal({ {option.label && {option.label}}
{formatPrice(option.price)}
{option.points} 积分
+
{option.validityDays === 0 ? '永久有效' : `${option.validityDays} 天内有效`}
))}