Thiết kế: NFT Management Feature
1. Tổng quan kiến trúc
flowchart TB
subgraph Client
App["Mobile App"]
end
subgraph Web["web module (port 8080)"]
HC["NftHunterController\n/api/nft-hunter/*"]
GC["NftGauntletController\n/api/nft-gauntlet/*"]
BBC["NftBountyBallController\n/api/nft-bounty-ball/*"]
end
subgraph Marketplace["webmarketplace module (port 8084)"]
RC["NftRentalController\n/api/nft-rental/*"]
end
subgraph Core["application-core"]
ROS["NftRentalOrderService"]
NHS["NftHunterService"]
NGS["NftGauntletService"]
NBS["NftBountyBallService"]
Coin["UserSystemCoinService"]
Node["NodeServerService"]
Notify["NotificationHandleService"]
Product["ProductService"]
end
subgraph Batch
EXPIRY["NftRentalExpirationCheckBatch\n(scheduled)"]
end
App --> HC & GC & BBC
App --> RC
HC --> NHS
GC --> NGS
BBC --> NBS
RC --> ROS
ROS --> NBS & Coin & Node & Notify & Product
EXPIRY --> ROS
2. Rental order sequence - confirm rent
sequenceDiagram
participant C as Renter App
participant RC as NftRentalController
participant ROS as NftRentalOrderService
participant NBS as NftBountyBallService
participant Coin as UserSystemCoinService
participant Node as NodeServerService
participant Notify as NotificationHandleService
participant DB as MySQL
C->>RC: PUT /api/nft-rental/confirm-rent/{rentalOrderId}
RC->>ROS: confirmNftRental(renterId, orderId)
ROS->>DB: findRentalOrder(orderId)
ROS->>ROS: validate status == NEW
ROS->>ROS: validate renter eligibility (max active rentals, etc.)
ROS->>Coin: charge renter B-Coin (rental_fee)
ROS->>Coin: deposit owner B-Coin (fee - admin_fee)
ROS->>Coin: admin fee deduction
ROS->>DB: update order: status=RENTING, renterId, startTime, endTime
ROS->>DB: create NftRentalOrderHistory record
alt NFT is minted (on-chain)
ROS->>Node: confirmSolanaNftRental(orderId, ...)
end
ROS->>Notify: notify owner "NFT rented out"
ROS-->>RC: void
RC-->>C: Result.OK()
3. Rental order state machine
stateDiagram-v2
[*] --> NEW: POST /api/nft-rental\n(owner lists NFT)
NEW --> NEW_VERIFIED: verify-order-creation success
NEW --> CANCELLED: verify-order-creation fail\n(on-chain reject)
NEW --> CANCELLED: cancel-rental-order
NEW_VERIFIED --> RENTING: confirm-rent\n(renter confirms)
RENTING --> FINISHED: rental_end_time reached\n(batch processExpiredRental)
RENTING --> RETURN_EARLY: return-rental-early\n(renter returns early)
RENTING --> EXPIRED_HANDLED: NftRentalExpirationCheckBatch
FINISHED --> [*]
RETURN_EARLY --> [*]
EXPIRED_HANDLED --> [*]
CANCELLED --> [*]
note right of NEW: product.status = RENTING
note right of CANCELLED: product.status = AVAILABLE
note right of FINISHED: product.status = AVAILABLE\nowner notified
4. Product status sync
flowchart LR
Available["AVAILABLE"] -->|"Create rental order"| Renting["RENTING"]
Renting -->|"Rental finishes/returns/expires"| Available
Renting -->|"verify fail"| Available
Available -->|"In gameplay"| InGame["IN_GAME"]
InGame -->|"Game ends"| Available
5. Minted vs unminted NFT branches
// In NftRentalOrderService.confirmNftRental():
public void confirmNftRental(String renterId, String orderId) {
// ... coin transactions, DB updates ...
NftBountyBallModel ball = nftBountyBallService.findById(order.getBountyBallId());
if (ball.isMinted()) {
// On-chain: notify node server to update blockchain state
nodeServerService.confirmSolanaNftRental(
order.getId(),
ball.getTokenId(),
renter.getPublicAddress()
);
}
// If not minted: DB-only, no blockchain call needed
}
6. Rental fee calculation
// NftRentalController.getListRentalDuration()
BigDecimal coefficientA = getCoefficientA(rarity); // per rarity
BigDecimal rentalFeeSuggest = coefficientA.multiply(BigDecimal.valueOf(duration));
// getCoefficientA():
COMMON → 50
UNCOMMON → 100
RARE → 300
EPIC → 500
LEGENDARY → 1000
7. Market rental filter logic
// NftRentalController.getMarketRentalPageRentalOrder()
FilterNftRentalOrder filter = new FilterNftRentalOrder();
filter.setRentalStatus(NftEnum.RentalOrderStatus.NEW); // only available listings
filter.setOwnerId("!" + loginUserId); // exclude own listings (! prefix = NOT equal)
// Sort options: NEWEST (default) or other RentalOrderSortType values
// Lang key is required for ball localization
8. Error handling
| Situation |
Exception |
Message |
| Rental order không tìm thấy |
BadRequestException |
RENTAL_ORDER_NOT_FOUND |
| NFT information không tìm thấy |
BadRequestException |
NFT_INFORMATION_NOT_FOUND |
| Rental order history không tìm thấy |
BadRequestException |
RENTAL_ORDER_HISTORY_NOT_FOUND |
Thiếu lang_key param |
BadRequestException |
LANG_KEY_IS_INVALID |
| Invalid sort value |
BadRequestException |
INVALID_SORT_VALUE |
| Owner không tồn tại |
NotfoundException |
USER_NOT_FOUND |
9. Điểm cần chú ý
| # |
Vấn đề |
Chi tiết |
| 1 |
testChangeOwner endpoint |
Test endpoint không nên tồn tại trên production |
| 2 |
finishRental endpoint |
Cũng là test endpoint (PUT /finish-rental-order/{id}) |
| 3 |
Admin fee logic |
Xác nhận admin fee percentage/flat được config ở đâu |
| 4 |
Concurrent confirm |
Race condition khi 2 renters confirm cùng lúc → cần transaction lock |