// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "./interfaces/ICoupon.sol"; import "./interfaces/ICompliance.sol"; /// @title Redemption — 兑付合约 /// @notice 消费者直接与发行方结算,平台不介入;兑付时销毁券 /// @dev 使用 Transparent Proxy 部署 contract Redemption is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); ICoupon public couponContract; ICompliance public compliance; // 兑付记录 struct RedemptionRecord { uint256 tokenId; address consumer; address issuer; bytes32 storeId; uint256 redeemedAt; } uint256 public totalRedemptions; mapping(uint256 => RedemptionRecord) public redemptions; // redemptionId → record // --- Events --- event CouponRedeemed( uint256 indexed tokenId, address indexed consumer, address indexed issuer, bytes32 storeId, uint256 redemptionId ); function initialize( address _couponContract, address _compliance, address admin ) external initializer { __AccessControl_init(); __ReentrancyGuard_init(); couponContract = ICoupon(_couponContract); compliance = ICompliance(_compliance); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(ADMIN_ROLE, admin); } /// @notice 消费者兑付券 — 销毁 NFT,触发事件通知发行方 /// @param tokenId 券 token ID /// @param storeId 门店 ID(bytes32 哈希) function redeem(uint256 tokenId, bytes32 storeId) external nonReentrant { // 所有权验证 require(couponContract.ownerOf(tokenId) == msg.sender, "Redemption: not owner"); // 合规检查 require(!compliance.isBlacklisted(msg.sender), "Redemption: blacklisted"); ICoupon.CouponConfig memory config = couponContract.getConfig(tokenId); // 到期验证 require(block.timestamp <= config.expiryDate, "Redemption: coupon expired"); // 门店限定验证 if (config.allowedStores.length > 0) { require(_isAllowedStore(storeId, config.allowedStores), "Redemption: store not allowed"); } // 销毁券(burn) couponContract.burn(tokenId); // 记录兑付 uint256 redemptionId = totalRedemptions++; redemptions[redemptionId] = RedemptionRecord({ tokenId: tokenId, consumer: msg.sender, issuer: config.issuer, storeId: storeId, redeemedAt: block.timestamp }); // 通知发行方(event),平台不介入消费数据 emit CouponRedeemed(tokenId, msg.sender, config.issuer, storeId, redemptionId); } /// @notice 查询兑付记录 function getRedemption(uint256 redemptionId) external view returns (RedemptionRecord memory) { return redemptions[redemptionId]; } // --- Internal --- /// @dev 检查门店是否在允许列表中 function _isAllowedStore(bytes32 storeId, bytes32[] memory allowedStores) internal pure returns (bool) { for (uint256 i = 0; i < allowedStores.length; i++) { if (allowedStores[i] == storeId) return true; } return false; } }