Token 交易平台 P1 实施计划(Java 版)

_
# Token 交易平台 P1 实施计划(Java Spring Boot 版) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. **Goal:** 在 P0 基础上引入积分账户深化、市场竞价撮合、动态参考价、高级验证探针。 **Architecture:** 复用 P0 的 Spring Boot + MyBatis-Plus 架构,新增竞价引擎模块,参考价系统升级为动态计算。 **Tech Stack:** Java 21, Spring Boot 3.2, Maven, MyBatis-Plus, MySQL 8, Redis, Sa-Token **前置依赖:** P0 所有模块已完成并运行稳定 **模块依赖顺序:** ``` 模块1: 积分账户深化(积分互转、邀请返利、会员等级) ↓ 模块2: 市场竞价撮合引擎 ↓ 模块3: 动态参考价系统(历史成交数据驱动) ↓ 模块4: 高级验证探针(上下文长度测试 + 推理能力探针) ↓ 模块5: 信誉等级系统 + 评价系统 ``` --- ## 模块1:积分账户深化 ### Task 1.1: 积分互转功能 **Files:** - Create: `src/main/java/com/danke/tokenexchange/dto/TransferRequest.java` - Modify: `src/main/java/com/danke/tokenexchange/service/CreditService.java` - Modify: `src/main/java/com/danke/tokenexchange/controller/CreditController.java` - Create: `src/main/resources/db/migration/V6__add_transfer_type.sql` - [ ] **Step 1: 编写 Flyway V6** ```sql -- V6__add_transfer_type.sql -- No schema change needed, 'transfer' type already in tx type enum (VARCHAR) -- Add index for faster transfer lookups CREATE INDEX idx_credit_tx_type ON credit_transactions(type); ``` - [ ] **Step 2: 编写 TransferRequest DTO** ```java // src/main/java/com/danke/tokenexchange/dto/TransferRequest.java package com.danke.tokenexchange.dto; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import lombok.Data; @Data public class TransferRequest { @NotNull(message = "目标用户ID不能为空") private Long targetUserId; @NotNull(message = "转账金额不能为空") @Positive(message = "金额必须大于0") private Long amount; private String remark; } ``` - [ ] **Step 3: 在 CreditService 中添加转账方法** ```java @Transactional public void transfer(Long fromUserId, Long toUserId, Long amount, String remark) { if (fromUserId.equals(toUserId)) { throw new BusinessException(400, "不能给自己转账"); } // Deduct from sender CreditTransaction txOut = deductCredits(fromUserId, amount, "transfer", String.valueOf(toUserId), "转账给用户" + toUserId + ": " + remark); // Add to receiver CreditTransaction txIn = addCredits(toUserId, amount, "transfer", String.valueOf(fromUserId), "收到用户" + fromUserId + "转账: " + remark); } ``` - [ ] **Step 4: 在 CreditController 添加转账接口** ```java @PostMapping("/transfer") public Result transfer(@Valid @RequestBody TransferRequest request) { Long fromUserId = StpUtil.getLoginIdAsLong(); creditService.transfer(fromUserId, request.getTargetUserId(), request.getAmount(), request.getRemark()); return Result.success("转账成功"); } ``` - [ ] **Step 5: Commit** ```bash git add . git commit -m "feat(P1): add credit transfer between users" ``` --- ### Task 1.2: 邀请返利机制 **Files:** - Modify: `src/main/java/com/danke/tokenexchange/entity/User.java` - Create: `src/main/resources/db/migration/V7__add_inviter_to_users.sql` - Modify: `src/main/java/com/danke/tokenexchange/service/UserService.java` - [ ] **Step 1: 编写 Flyway V7** ```sql -- V7__add_inviter_to_users.sql ALTER TABLE users ADD COLUMN inviter_id BIGINT DEFAULT NULL AFTER id; ALTER TABLE users ADD COLUMN invite_code VARCHAR(20) DEFAULT NULL UNIQUE AFTER inviter_id; CREATE INDEX idx_users_inviter ON users(inviter_id); ``` - [ ] **Step 2: 更新 User 实体** ```java private Long inviterId; private String inviteCode; ``` - [ ] **Step 3: 在 UserService 中添加邀请逻辑** ```java public User register(RegisterRequest request, String inviteCode) { // ... existing register logic ... // Check inviter if (inviteCode != null && !inviteCode.isEmpty()) { User inviter = userMapper.selectOne( new QueryWrapper().eq("invite_code", inviteCode)); if (inviter != null) { user.setInviterId(inviter.getId()); // Give inviter bonus creditService.addCredits(inviter.getId(), 500L, "invite_reward", String.valueOf(user.getId()), "邀请奖励:用户" + user.getUsername()); } } // Generate invite code for new user user.setInviteCode(generateInviteCode()); // ... rest of register logic ... } private String generateInviteCode() { return "U" + System.currentTimeMillis() % 100000000; } ``` - [ ] **Step 4: Commit** ```bash git add . git commit -m "feat(P1): add invite code and referral bonus system" ``` --- ### Task 1.3: 会员等级与费率折扣 **Files:** - Create: `src/main/java/com/danke/tokenexchange/entity/MembershipLevel.java` - Create: `src/main/java/com/danke/tokenexchange/service/MembershipService.java` - Modify: `src/main/java/com/danke/tokenexchange/entity/User.java` - [ ] **Step 1: 编写会员等级枚举** ```java // src/main/java/com/danke/tokenexchange/entity/MembershipLevel.java package com.danke.tokenexchange.entity; import lombok.Getter; @Getter public enum MembershipLevel { NOVICE(0, 50, 0.20, 1000L), TRUSTED(50, 150, 0.10, 5000L), SENIOR(150, 300, 0.05, 20000L), ELITE(300, Integer.MAX_VALUE, 0.03, 100000L); private final int minScore; private final int maxScore; private final double depositRate; private final long tradeLimit; MembershipLevel(int minScore, int maxScore, double depositRate, long tradeLimit) { this.minScore = minScore; this.maxScore = maxScore; this.depositRate = depositRate; this.tradeLimit = tradeLimit; } public static MembershipLevel fromReputation(int reputation) { for (MembershipLevel level : values()) { if (reputation >= level.minScore && reputation < level.maxScore) { return level; } } return NOVICE; } } ``` - [ ] **Step 2: Commit** ```bash git add . git commit -m "feat(P1): add membership level system with deposit rates and trade limits" ``` --- ## 模块2:市场竞价撮合引擎 ### Task 2.1: 竞价订单模型 **Files:** - Create: `src/main/java/com/danke/tokenexchange/entity/Bid.java` - Create: `src/main/resources/db/migration/V8__create_bids.sql` - Create: `src/main/java/com/danke/tokenexchange/mapper/BidMapper.java` - [ ] **Step 1: 编写 Flyway V8** ```sql -- V8__create_bids.sql CREATE TABLE IF NOT EXISTS bids ( id BIGINT AUTO_INCREMENT PRIMARY KEY, bid_number VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, type VARCHAR(10) NOT NULL COMMENT 'buy, sell', model VARCHAR(100) NOT NULL, amount BIGINT NOT NULL COMMENT 'token数量', price_per_token DECIMAL(20, 10) NOT NULL COMMENT '每token积分价格', total_price BIGINT NOT NULL COMMENT '总积分价格', status VARCHAR(20) DEFAULT 'active' COMMENT 'active, matched, cancelled, expired', matched_with BIGINT DEFAULT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NULL, INDEX idx_bids_model (model), INDEX idx_bids_type (type), INDEX idx_bids_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` - [ ] **Step 2: 编写 Bid 实体** ```java // src/main/java/com/danke/tokenexchange/entity/Bid.java package com.danke.tokenexchange.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("bids") public class Bid { @TableId(type = IdType.AUTO) private Long id; private String bidNumber; private Long userId; private String type; // buy, sell private String model; private Long amount; private BigDecimal pricePerToken; private Long totalPrice; private String status; private Long matchedWith; private LocalDateTime createdAt; private LocalDateTime expiresAt; } ``` - [ ] **Step 3: Commit** ```bash git add . git commit -m "feat(P1): add Bid entity and migration for market matching" ``` --- ### Task 2.2: 撮合引擎 Service **Files:** - Create: `src/main/java/com/danke/tokenexchange/service/MatchingEngineService.java` - Create: `src/main/java/com/danke/tokenexchange/dto/PlaceBidRequest.java` - [ ] **Step 1: 编写撮合引擎** ```java // src/main/java/com/danke/tokenexchange/service/MatchingEngineService.java package com.danke.tokenexchange.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.danke.tokenexchange.common.exception.BusinessException; import com.danke.tokenexchange.dto.PlaceBidRequest; import com.danke.tokenexchange.entity.Bid; import com.danke.tokenexchange.mapper.BidMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.List; @Service @RequiredArgsConstructor public class MatchingEngineService { private final BidMapper bidMapper; private final CreditService creditService; private final PricingService pricingService; @Transactional public Bid placeBid(Long userId, PlaceBidRequest request) { // Validate price deviation from reference BigDecimal referenceRate; try { referenceRate = pricingService.getReferenceRate(request.getModel(), "credit"); } catch (Exception e) { referenceRate = BigDecimal.ONE; } BigDecimal deviation = request.getPricePerToken() .subtract(referenceRate) .abs() .divide(referenceRate, 10, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); // P1: warn on >30%, block on >50% if (deviation.compareTo(new BigDecimal("50")) > 0) { throw new BusinessException(400, String.format( "价格偏离参考价 %.2f%%,超过50%%上限,需平台审核", deviation)); } // Lock credits for buyer if ("buy".equals(request.getType())) { long total = request.getPricePerToken() .multiply(new BigDecimal(request.getAmount())) .setScale(0, RoundingMode.HALF_UP) .longValue(); creditService.deductCredits(userId, total, "bid_lock", "", "竞价冻结"); } Bid bid = new Bid(); bid.setBidNumber("BID" + System.currentTimeMillis()); bid.setUserId(userId); bid.setType(request.getType()); bid.setModel(request.getModel()); bid.setAmount(request.getAmount()); bid.setPricePerToken(request.getPricePerToken()); bid.setTotalPrice(request.getPricePerToken() .multiply(new BigDecimal(request.getAmount())) .setScale(0, RoundingMode.HALF_UP) .longValue()); bid.setStatus("active"); bid.setExpiresAt(LocalDateTime.now().plusDays(7)); bidMapper.insert(bid); // Try to match immediately tryMatch(bid); return bid; } @Transactional public void tryMatch(Bid bid) { if (!"active".equals(bid.getStatus())) return; String oppositeType = "buy".equals(bid.getType()) ? "sell" : "buy"; // Find matching bids: same model, opposite type, overlapping price QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("model", bid.getModel()) .eq("type", oppositeType) .eq("status", "active") .gt("expires_at", LocalDateTime.now()); if ("buy".equals(bid.getType())) { // Buyer wants to buy at price X, find seller at price <= X wrapper.le("price_per_token", bid.getPricePerToken()); } else { // Seller wants to sell at price Y, find buyer at price >= Y wrapper.ge("price_per_token", bid.getPricePerToken()); } wrapper.orderByAsc("created_at"); // FIFO matching wrapper.last("LIMIT 1"); Bid match = bidMapper.selectOne(wrapper); if (match == null) return; // Execute match long tradeAmount = Math.min(bid.getAmount(), match.getAmount()); BigDecimal tradePrice = "buy".equals(bid.getType()) ? bid.getPricePerToken().min(match.getPricePerToken()) : bid.getPricePerToken().max(match.getPricePerToken()); // Update bid statuses bid.setStatus("matched"); bid.setMatchedWith(match.getId()); bidMapper.updateById(bid); match.setStatus("matched"); match.setMatchedWith(bid.getId()); bidMapper.updateById(match); // Handle settlement long totalPrice = tradePrice.multiply(new BigDecimal(tradeAmount)) .setScale(0, RoundingMode.HALF_UP) .longValue(); if ("buy".equals(bid.getType())) { // Buyer pays, seller receives // Refund locked credits difference long locked = bid.getTotalPrice(); if (locked > totalPrice) { creditService.addCredits(bid.getUserId(), locked - totalPrice, "bid_refund", "", "竞价差额退还"); } creditService.deductCredits(bid.getUserId(), totalPrice, "trade", "", "竞价成交"); creditService.addCredits(match.getUserId(), totalPrice, "trade", "", "竞价成交"); } else { // Seller was selling, buyer pays creditService.deductCredits(match.getUserId(), totalPrice, "trade", "", "竞价成交"); creditService.addCredits(bid.getUserId(), totalPrice, "trade", "", "竞价成交"); } } public List listActiveBids(String model, String type) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("status", "active") .gt("expires_at", LocalDateTime.now()) .orderByDesc("created_at"); if (model != null) wrapper.eq("model", model); if (type != null) wrapper.eq("type", type); return bidMapper.selectList(wrapper); } } ``` - [ ] **Step 2: Commit** ```bash git add . git commit -m "feat(P1): add market matching engine with price deviation check" ``` --- ## 模块3:动态参考价系统 ### Task 3.1: 成交数据收集与参考价更新 **Files:** - Create: `src/main/java/com/danke/tokenexchange/entity/TradeRecord.java` - Create: `src/main/resources/db/migration/V9__create_trade_records.sql` - Create: `src/main/java/com/danke/tokenexchange/mapper/TradeRecordMapper.java` - Modify: `src/main/java/com/danke/tokenexchange/service/PricingService.java` - [ ] **Step 1: 编写 Flyway V9** ```sql -- V9__create_trade_records.sql CREATE TABLE IF NOT EXISTS trade_records ( id BIGINT AUTO_INCREMENT PRIMARY KEY, model_a VARCHAR(100) NOT NULL, model_b VARCHAR(100) NOT NULL, traded_amount BIGINT NOT NULL, actual_rate DECIMAL(20, 10) NOT NULL, reference_rate DECIMAL(20, 10) NOT NULL, deviation_percent DECIMAL(10, 4) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_trades_models (model_a, model_b), INDEX idx_trades_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` - [ ] **Step 2: 编写 TradeRecord 实体和 Mapper** ```java // src/main/java/com/danke/tokenexchange/entity/TradeRecord.java package com.danke.tokenexchange.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("trade_records") public class TradeRecord { @TableId(type = IdType.AUTO) private Long id; private String modelA; private String modelB; private Long tradedAmount; private BigDecimal actualRate; private BigDecimal referenceRate; private BigDecimal deviationPercent; private LocalDateTime createdAt; } ``` ```java // src/main/java/com/danke/tokenexchange/mapper/TradeRecordMapper.java package com.danke.tokenexchange.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.danke.tokenexchange.entity.TradeRecord; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.math.BigDecimal; import java.time.LocalDateTime; @Mapper public interface TradeRecordMapper extends BaseMapper { @Select("SELECT AVG(actual_rate) FROM trade_records WHERE model_a = #{modelA} AND model_b = #{modelB} AND created_at >= #{since}") BigDecimal selectAverageRate(String modelA, String modelB, LocalDateTime since); } ``` - [ ] **Step 3: 更新 PricingService 添加动态调整** ```java private final TradeRecordMapper tradeRecordMapper; public BigDecimal getReferenceRate(String modelA, String modelB) { // ... existing base rate calculation ... // P1: Market adjustment based on last 7 days trades LocalDateTime since = LocalDateTime.now().minusDays(7); BigDecimal marketAvg = tradeRecordMapper.selectAverageRate(modelA, modelB, since); if (marketAvg != null && marketAvg.compareTo(BigDecimal.ZERO) > 0) { // Blend 70% official + 30% market BigDecimal blended = referenceRate.multiply(new BigDecimal("0.7")) .add(marketAvg.multiply(new BigDecimal("0.3"))); return blended.setScale(6, RoundingMode.HALF_UP); } return referenceRate; } ``` - [ ] **Step 4: Commit** ```bash git add . git commit -m "feat(P1): add dynamic reference pricing with 7-day market data blending" ``` --- ## 模块4:高级验证探针 ### Task 4.1: 上下文长度测试 **Files:** - Modify: `src/main/java/com/danke/tokenexchange/service/VerificationService.java` - [ ] **Step 1: 添加上下文长度测试方法** ```java public ContextTestResult testContextLength(String apiKey, String modelName, String provider, int claimedLength) { // Test at 25%, 50%, 75%, 100% of claimed length int[] testPoints = { claimedLength / 4, claimedLength / 2, claimedLength * 3 / 4, claimedLength }; int maxWorking = 0; StringBuilder notes = new StringBuilder(); for (int testLen : testPoints) { if (testLen <= 0) continue; // Generate a prompt of approximately testLen tokens // Simple approach: repeated text ~4 chars per token String prompt = generateTestPrompt(testLen); boolean works = sendTestRequest(apiKey, modelName, provider, prompt); if (works) { maxWorking = testLen; } else { notes.append("Failed at ").append(testLen).append(" tokens; "); break; } } return new ContextTestResult(claimedLength, maxWorking, notes.toString()); } private String generateTestPrompt(int targetTokens) { // Approximate: 1 token ~ 4 chars for English, ~1.5 for Chinese StringBuilder sb = new StringBuilder(); String word = "test "; int wordsNeeded = targetTokens; // rough approximation for (int i = 0; i < wordsNeeded; i++) { sb.append(word); } return sb.toString().trim(); } private boolean sendTestRequest(String apiKey, String modelName, String provider, String prompt) { try { String baseUrl = getBaseUrl(provider); String requestBody = String.format( "{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],\"max_tokens\":10}", modelName, prompt.replace("\"", "\\\"")); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + apiKey); headers.setContentType(MediaType.APPLICATION_JSON); ResponseEntity response = restTemplate.postForEntity( baseUrl + "/chat/completions", new HttpEntity<>(requestBody, headers), String.class ); if (response.getStatusCode() == HttpStatus.OK) { // Check if response indicates truncation or error return !response.getBody().contains("\"finish_reason\":\"length\""); } return false; } catch (Exception e) { return false; } } public record ContextTestResult(int claimedLength, int testedMax, String notes) {} ``` - [ ] **Step 2: Commit** ```bash git add . git commit -m "feat(P1): add context length testing probe" ``` --- ## 模块5:信誉等级系统 + 评价系统 ### Task 5.1: 评价实体与 Service **Files:** - Create: `src/main/java/com/danke/tokenexchange/entity/Review.java` - Create: `src/main/resources/db/migration/V10__create_reviews.sql` - Create: `src/main/java/com/danke/tokenexchange/mapper/ReviewMapper.java` - Create: `src/main/java/com/danke/tokenexchange/service/ReviewService.java` - [ ] **Step 1: 编写 Flyway V10** ```sql -- V10__create_reviews.sql CREATE TABLE IF NOT EXISTS reviews ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, reviewer_id BIGINT NOT NULL, reviewee_id BIGINT NOT NULL, rating INT NOT NULL COMMENT '1-5 stars', tags VARCHAR(255) DEFAULT '' COMMENT 'comma separated tags', comment VARCHAR(1000) DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` - [ ] **Step 2: 编写 Review 实体** ```java // src/main/java/com/danke/tokenexchange/entity/Review.java package com.danke.tokenexchange.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("reviews") public class Review { @TableId(type = IdType.AUTO) private Long id; private Long orderId; private Long reviewerId; private Long revieweeId; private Integer rating; private String tags; private String comment; private LocalDateTime createdAt; } ``` - [ ] **Step 3: 编写 ReviewService** ```java // src/main/java/com/danke/tokenexchange/service/ReviewService.java package com.danke.tokenexchange.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.danke.tokenexchange.entity.Review; import com.danke.tokenexchange.mapper.ReviewMapper; import com.danke.tokenexchange.mapper.UserMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class ReviewService { private final ReviewMapper reviewMapper; private final UserMapper userMapper; @Transactional public Review createReview(Long reviewerId, Long orderId, Long revieweeId, Integer rating, String tags, String comment) { Review review = new Review(); review.setOrderId(orderId); review.setReviewerId(reviewerId); review.setRevieweeId(revieweeId); review.setRating(rating); review.setTags(tags); review.setComment(comment); reviewMapper.insert(review); // Update reputation updateReputation(revieweeId); return review; } private void updateReputation(Long userId) { // Count reviews Long count = reviewMapper.selectCount( new QueryWrapper().eq("reviewee_id", userId)); // Calculate average rating var reviews = reviewMapper.selectList( new QueryWrapper().eq("reviewee_id", userId)); double avgRating = reviews.stream() .mapToInt(Review::getRating) .average() .orElse(5.0); // Simple formula: base 100 + successful trades * 2 + avg rating * 10 // TODO: integrate with successful trade count int newReputation = 100 + count.intValue() * 2 + (int)(avgRating * 10); var user = userMapper.selectById(userId); if (user != null) { user.setReputation(Math.min(newReputation, 500)); userMapper.updateById(user); } } } ``` - [ ] **Step 4: Commit** ```bash git add . git commit -m "feat(P1): add review and rating system with reputation updates" ``` --- ## P1 总结 | 模块 | 新增能力 | |------|---------| | 积分深化 | 积分互转、邀请返利、会员等级 | | 市场竞价 | 买卖挂单、FIFO撮合、价格偏离检查 | | 动态定价 | 7天成交数据加权、市场供需调整 | | 高级验证 | 上下文长度测试 | | 评价系统 | 评分、标签、信誉分自动计算 | --- **P1 计划完成。** 继续 P2。
Token 交易平台 P0 MVP 实施计划(Java 版) 2026-04-21
Token 交易平台 P2 实施计划(Java 版) 2026-04-21

评论区