oneclub-member-shop
Version:
Shared 1club member center & points market components
439 lines (382 loc) • 13.9 kB
JSX
// src/SingleProductRedeemPanel.jsx
import React, { useEffect, useMemo, useState } from "react";
import { Card, Button, Form, InputGroup } from "react-bootstrap";
import {
getCurrentMember,
setCurrentMember,
} from "./hooks/useMemberAuth";
import SuccessModal from "./components/SuccessModal"; // ★ 新增:成功弹窗
/**
* 单品兑换面板:给 Media360 用的“现金或360币支付”
*
* Props:
* - cmsEndpoint : Strapi API 基地址
* - cmsApiKey : Strapi 只读/写入 token
* - couponEndpoint : 优惠券系统地址,比如 https://server.coupon.do360.com
* - emailEndpoint : 邮件服务地址
* - product: {
* Name, // 商品名
* Price, // 价格(现金点)
* MaxDeduction, // 最大可抵扣点数
* Description, // 商品描述
* ProviderName, // ★ 必填:发券方(必须和 CouponSysAccount.Name 完全一致)
* }
* - onSuccess() : 兑换成功后的回调(可选)
*/
export default function SingleProductRedeemPanel({
cmsEndpoint,
cmsApiKey,
couponEndpoint,
emailEndpoint,
product,
onSuccess,
}) {
const currUser = getCurrentMember() || {};
const isLoggedIn = !!currUser?.number;
const [deduction, setDeduction] = useState(0);
const [loading, setLoading] = useState(false);
const [showSuccessModal, setShowSuccessModal] = useState(false); // ★ 新增:控制成功弹窗
// ★★★ 组件加载时自动从 Strapi 刷新一次积分 & 写 cookie
useEffect(() => {
async function refreshMemberBalance() {
const user = getCurrentMember() || {};
if (!cmsEndpoint || !cmsApiKey) return;
if (!user.number) return; // 未登录就不查
try {
const qs = new URLSearchParams();
qs.append("filters[MembershipNumber][$eq]", String(user.number));
const url = `${cmsEndpoint}/api/one-club-memberships?${qs.toString()}`;
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cmsApiKey}`,
},
});
if (!res.ok) return;
const json = await res.json();
const record = json?.data?.[0];
if (!record) return;
const refreshed = {
...user,
points: Number(record.Point ?? user.points ?? 0),
discount_point: Number(
record.DiscountPoint ?? user.discount_point ?? 0
),
loyalty_point: Number(
record.LoyaltyPoint ?? user.loyalty_point ?? 0
),
};
setCurrentMember(refreshed);
console.log(
"[SingleProductRedeemPanel] refreshed member balance from server"
);
} catch (err) {
console.error(
"[SingleProductRedeemPanel] refreshMemberBalance error:",
err
);
}
}
refreshMemberBalance();
}, [cmsEndpoint, cmsApiKey]);
const price = Number(product?.Price || 0);
const maxDeduction = useMemo(
() => Math.min(Number(product?.MaxDeduction || 0), price),
[price, product]
);
const cash = currUser?.points || 0;
const discountPoint = currUser?.discount_point || 0;
// ✅ 本次实际支付金额(和 MemberPointMarket 保持一致的展示逻辑)
const cashToPay = price - deduction; // 现金:价格 - 抵扣
const pointsToUse = deduction; // 360币:抵扣多少就是用多少
// ✅ 兑换后的余额
const remainingCash = cash - cashToPay;
const remainingDiscount = discountPoint - pointsToUse;
const sufficientCash = cash >= cashToPay;
const sufficientDiscount = remainingDiscount >= 0;
const canRedeem =
isLoggedIn && sufficientCash && sufficientDiscount && !loading;
const handleDeductionInput = (value) => {
let n = Number(value);
if (Number.isNaN(n)) n = 0;
if (n < 0) n = 0;
if (n > maxDeduction) n = maxDeduction;
setDeduction(n);
};
/**
* 更新 Strapi 里的积分 + MyCoupon,并同步 cookie
* (结构和 MemberPointMarket.jsx 保持一致,改用 documentId)
*/
async function updateUserPoint(couponCid) {
const latestUser = getCurrentMember() || {};
if (!latestUser.number || !latestUser.email) {
console.error("Cannot update user points: missing number or email");
return;
}
// 根据会员号 + 邮箱查 membership 记录
const userQueryUrl =
`${cmsEndpoint}/api/one-club-memberships` +
`?filters[MembershipNumber][$eq]=${encodeURIComponent(
latestUser.number
)}` +
`&filters[Email][$eq]=${encodeURIComponent(
latestUser.email
)}` +
`&populate=MyCoupon`;
const userResponse = await fetch(userQueryUrl, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cmsApiKey}`,
},
});
if (!userResponse.ok) {
console.error("Failed to fetch membership record");
return;
}
const userJson = await userResponse.json();
const userRecord = userJson?.data?.[0];
if (!userRecord) {
console.error("Membership record not found");
return;
}
// ★ 关键:使用 documentId,而不是 id
const documentId = userRecord.documentId;
if (!documentId) {
console.error(
"[SingleProductRedeemPanel] membership record missing documentId"
);
return;
}
const currentPoint = Number(userRecord.Point || 0);
const currentDiscountPoint = Number(userRecord.DiscountPoint || 0);
// ✅ 按照“本次支付金额”扣减(和上面的 cashToPay / pointsToUse 一致)
const newPoint = currentPoint - cashToPay;
const newDiscountPoint = currentDiscountPoint - pointsToUse;
const existingCoupons =
userRecord.MyCoupon?.map((c) => c.documentId) ?? [];
const updatedCoupons = [...new Set([...existingCoupons, couponCid])];
const updatePayload = {
data: {
Point: newPoint,
DiscountPoint: newDiscountPoint,
MyCoupon: updatedCoupons,
},
};
const updateResponse = await fetch(
`${cmsEndpoint}/api/one-club-memberships/${documentId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cmsApiKey}`,
},
body: JSON.stringify(updatePayload),
}
);
if (!updateResponse.ok) {
const updateError = await updateResponse.json().catch(() => ({}));
console.error(
"Error updating user info:",
updateError?.error || updateError
);
} else {
console.log("Membership updated successfully");
}
// 更新 cookie 里的用户积分
const newUser = {
...latestUser,
points: newPoint,
discount_point: newDiscountPoint,
};
setCurrentMember(newUser);
}
/**
* 核心:创建 active coupon + 发送邮件 + 更新积分
*/
async function handleRedeem() {
if (!isLoggedIn) return;
setLoading(true);
try {
const latestUser = getCurrentMember() || {};
const expiryDate = new Date();
expiryDate.setFullYear(expiryDate.getFullYear() + 1);
// ★ 从 product.ProviderName 读取提供者名称
let assignedFrom = (product?.ProviderName || "").trim();
if (!assignedFrom) {
// 强烈建议业务方总是传 ProviderName;没传就兜底防炸
console.warn(
"[SingleProductRedeemPanel] product.ProviderName 为空,使用兜底提供者 '1Club'"
);
assignedFrom = "1Club";
}
const couponPayload = {
title: product.Name,
description: product.Description || "",
expiry: expiryDate.toISOString(),
assigned_from: assignedFrom, // 必须和 CouponSysAccount.Name 一致
assigned_to: latestUser.name,
value: cashToPay, // ✅ 只付“现金部分”
};
console.log("couponPayload sending:", couponPayload);
// 1) 在优惠券系统创建 active 券
const couponResponse = await fetch(
`${couponEndpoint}/create-active-coupon`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(couponPayload),
mode: "cors",
credentials: "include",
}
);
const couponData = await couponResponse.json();
if (couponResponse.ok && couponData.couponStatus === "active") {
const QRdata = couponData.QRdata;
// 2) 调用邮件服务发送券
const emailPayload = {
name: latestUser.name,
email: latestUser.email,
data: QRdata,
title: product.Name,
};
const emailResponse = await fetch(
`${emailEndpoint}/1club/coupon_distribute`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(emailPayload),
mode: "cors",
credentials: "include",
}
);
if (emailResponse.ok) {
// 3) 券发出成功之后,更新积分 + MyCoupon
await updateUserPoint(couponData.cid);
console.log("Redeemed.");
setLoading(false);
setDeduction(0);
// ★ 改成弹窗,不再使用 alert
setShowSuccessModal(true);
} else {
const emailError = await emailResponse.json().catch(() => ({}));
console.error("Email API error:", emailError.message);
setLoading(false);
setDeduction(0);
alert("兑换失败(发券邮件失败),请稍后重试。");
}
} else {
console.error("Coupon system error:", couponData);
setLoading(false);
setDeduction(0);
alert("兑换失败(券系统返回失败),请稍后重试。");
}
} catch (err) {
console.error("Redeem error:", err);
setLoading(false);
setDeduction(0);
alert("兑换失败,请稍后重试。");
}
}
return (
<>
<Card>
<Card.Body>
<h5 className="mb-3">确认兑换</h5>
<p>
商品:<b>{product?.Name}</b>
</p>
<p>价格:{price} 现金</p>
{isLoggedIn ? (
<>
{/* ✅ 左边显示本次支付金额,右边显示兑换后余额 */}
<p>
现金:{cashToPay} → 兑换后余额 <b>{remainingCash}</b>
</p>
<p>
360币:{pointsToUse} → 兑换后余额{" "}
<b>{remainingDiscount}</b>
</p>
{!sufficientCash && (
<p style={{ color: "red" }}>现金不足</p>
)}
{!sufficientDiscount && (
<p style={{ color: "red" }}>360币不足</p>
)}
{maxDeduction > 0 && (
<Form.Group className="mt-3">
<Form.Label>
点数抵扣 ({deduction}/{maxDeduction})
</Form.Label>
<Form.Range
min={0}
max={maxDeduction}
step={1}
value={deduction}
onChange={(e) =>
handleDeductionInput(e.target.value)
}
/>
<InputGroup className="mt-2">
<Form.Control
type="number"
min={0}
max={maxDeduction}
value={deduction}
onChange={(e) =>
handleDeductionInput(e.target.value)
}
/>
<Button
variant="outline-secondary"
onClick={() =>
handleDeductionInput(maxDeduction)
}
>
Max
</Button>
</InputGroup>
</Form.Group>
)}
<p className="mt-3">
注:兑换成功后的核销券有效期为一年,请注意哦!
</p>
</>
) : (
<p style={{ color: "red" }}>
请先登录会员中心再使用现金或 360 币支付。
</p>
)}
</Card.Body>
<Card.Footer>
<Button
variant={canRedeem ? "dark" : "secondary"}
className="w-100"
disabled={!canRedeem}
onClick={handleRedeem}
>
{loading
? "处理中..."
: !isLoggedIn
? "请先登录"
: sufficientCash && sufficientDiscount
? "确认兑换"
: !sufficientCash
? "现金不足"
: "360币不足"}
</Button>
</Card.Footer>
</Card>
{/* ✅ 兑换成功弹窗:标题 = 商品名,正文 = “兑换成功” */}
<SuccessModal
show={showSuccessModal}
title={product?.Name || "兑换成功"}
onHide={() => {
setShowSuccessModal(false);
if (onSuccess) onSuccess(); // 关闭弹窗后再执行回调(例如 navigate(-1))
}}
>
兑换成功
</SuccessModal>
</>
);
}