UNPKG

oneclub-member-shop

Version:

Shared 1club member center & points market components

439 lines (382 loc) 13.9 kB
// 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> </> ); }