feat(points): 充值档位添加积分有效期字段
- config/points-config.yaml: 每个档位添加 validity_days(7/30/90/180/365/0) - points.py: 支付回调和主动查询补单时根据档位配置设置 batch_expired_at - RechargeModal: 卡片展示有效期(永久有效 / N 天内有效)
This commit is contained in:
@@ -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} 通过主动查询确认支付成功,"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
# ── 免费业务(不扣积分)───────────────────────────────
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface RechargeOption {
|
||||
price: number; // 人民币(分)
|
||||
points: number; // 到账积分数
|
||||
label: string; // 标签,空字符串不显示
|
||||
validityDays: number; // 积分有效期(天),0 表示永久有效
|
||||
}
|
||||
|
||||
export interface RechargeOrder {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
<div key={`skeleton-${i}`} className="recharge-amount-card skeleton">
|
||||
<div className="skeleton-price" />
|
||||
<div className="skeleton-points" />
|
||||
<div className="skeleton-validity" />
|
||||
</div>
|
||||
))
|
||||
: options.map(option => (
|
||||
@@ -274,6 +276,7 @@ export default function RechargeModal({
|
||||
{option.label && <span className="tag">{option.label}</span>}
|
||||
<div className="price">{formatPrice(option.price)}</div>
|
||||
<div className="points">{option.points} 积分</div>
|
||||
<div className="validity">{option.validityDays === 0 ? '永久有效' : `${option.validityDays} 天内有效`}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user