# Sub-Plan A: Spring Boot 后端开发 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development > 前置依赖:Java 17+, Maven 3.8+, MySQL 8.0+, Redis **目标:** 搭建完整后端,提供小程序和管理端所有 API **数据库:** MySQL 8 数据库 `num_db`,Redis 用于配额计数 --- ## 项目骨架结构 ``` num-server/ ├── pom.xml └── src/main/java/com/num/ ├── NumApplication.java ├── common/ │ ├── Result.java # 统一响应 │ ├── GlobalExceptionHandler.java │ └── BizException.java ├── config/ │ ├── JwtConfig.java │ ├── WxPayConfig.java │ ├── DifyConfig.java │ └── WebMvcConfig.java # CORS + 拦截器 ├── interceptor/ │ ├── AuthInterceptor.java # 用户 Token 校验 │ └── AdminInterceptor.java # 管理员鉴权 ├── entity/ │ ├── User.java │ ├── Chart.java │ ├── ChatMessage.java │ ├── PaymentOrder.java │ ├── Commission.java │ ├── WithdrawRequest.java │ └── SystemConfig.java ├── repository/ # JPA Repository ├── service/ │ ├── UserService.java │ ├── ChartService.java │ ├── CalculatorService.java # Classic A 引擎 │ ├── ChatService.java │ ├── DifyProxyService.java │ ├── WxPayService.java │ ├── CommissionService.java │ └── ConfigService.java ├── controller/ │ ├── AuthController.java │ ├── ChartController.java │ ├── ChatController.java │ ├── PaymentController.java │ ├── CommissionController.java │ └── AdminController.java └── dto/ # Request/Response DTO ``` --- ### Task 1: 项目骨架 + 统一响应 + 数据库 **Files:** - Create: `pom.xml` - Create: `NumApplication.java` - Create: `common/Result.java` - Create: `common/BizException.java` - Create: `common/GlobalExceptionHandler.java` - Create: `application.yml` - Create: `schema.sql` - [ ] **Step 1: Create pom.xml** ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.1 com.num num-server 1.0.0 num-server Digital Energy Numerology Backend 17 org.springframework.bootspring-boot-starter-web org.springframework.bootspring-boot-starter-data-jpa org.springframework.bootspring-boot-starter-data-redis com.mysqlmysql-connector-jruntime io.jsonwebtokenjjwt-api0.12.3 io.jsonwebtokenjjwt-impl0.12.3runtime io.jsonwebtokenjjwt-jackson0.12.3runtime org.springframework.bootspring-boot-starter-testtest com.google.code.gsongson2.10.1 org.springframework.bootspring-boot-maven-plugin ``` - [ ] **Step 2: Create application.yml** ```yaml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/num_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: false redis: host: localhost port: 6379 timeout: 3000 num: jwt: secret: your-jwt-secret-key-change-in-production expire-hours: 720 wx: app-id: your_app_id app-secret: your_app_secret mch-id: your_mch_id mch-key: your_mch_key dify: base-url: https://your-dify-instance.com/v1 api-key: your_dify_api_key ``` - [ ] **Step 3: Create Result.java** ```java package com.num.common; public class Result { private int code; private String message; private T data; public static Result success(T data) { Result r = new Result<>(); r.code = 0; r.message = "success"; r.data = data; return r; } public static Result error(int code, String message) { Result r = new Result<>(); r.code = code; r.message = message; return r; } // getters/setters } ``` - [ ] **Step 4: Create BizException.java + GlobalExceptionHandler.java** ```java package com.num.common; public class BizException extends RuntimeException { private int code; public BizException(int code, String message) { super(message); this.code = code; } public int getCode() { return code; } } ``` ```java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public Result handleBiz(BizException e) { return Result.error(e.getCode(), e.getMessage()); } @ExceptionHandler(Exception.class) public Result handleEx(Exception e) { return Result.error(500, "服务器内部错误"); } } ``` - [ ] **Step 5: Create schema.sql(初始化 DB)** ```sql CREATE DATABASE IF NOT EXISTS num_db DEFAULT CHARSET utf8mb4; ``` - [ ] **Step 6: 验证启动** Run: `mvn spring-boot:run` Expected: 启动成功,无报错,访问 `http://localhost:8080/` 返回 404(正常) - [ ] **Step 7: Commit** --- ### Task 2: JWT 配置 + Auth 拦截器 **Files:** - Create: `config/JwtConfig.java` - Create: `config/WebMvcConfig.java` - Create: `interceptor/AuthInterceptor.java` - Create: `interceptor/AdminInterceptor.java` - [ ] **Step 1: Create JwtConfig.java** ```java package com.num.config; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; @Component public class JwtConfig { @Value("${num.jwt.secret}") private String secret; @Value("${num.jwt.expire-hours}") private long expireHours; private SecretKey getKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generate(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expireHours * 3600 * 1000)) .signWith(getKey()) .compact(); } public Claims parse(String token) { return Jwts.parser().verifyWith(getKey()).build() .parseSignedClaims(token).getPayload(); } } ``` - [ ] **Step 2: Create AuthInterceptor.java** ```java package com.num.interceptor; import com.num.common.BizException; import com.num.config.JwtConfig; import io.jsonwebtoken.Claims; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @Component public class AuthInterceptor implements HandlerInterceptor { private final JwtConfig jwtConfig; public AuthInterceptor(JwtConfig jwtConfig) { this.jwtConfig = jwtConfig; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String auth = request.getHeader("Authorization"); if (auth == null || !auth.startsWith("Bearer ")) { throw new BizException(1001, "Token is required"); } try { Claims claims = jwtConfig.parse(auth.substring(7)); request.setAttribute("userId", claims.get("userId", Long.class)); request.setAttribute("role", claims.get("role", String.class)); return true; } catch (Exception e) { throw new BizException(1001, "Token invalid"); } } } ``` - [ ] **Step 3: Create AdminInterceptor.java** ```java package com.num.interceptor; import com.num.common.BizException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @Component public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String role = (String) request.getAttribute("role"); if (!"admin".equals(role)) { throw new BizException(1001, "Admin access required"); } return true; } } ``` - [ ] **Step 4: Create WebMvcConfig.java** ```java package com.num.config; import com.num.interceptor.AdminInterceptor; import com.num.interceptor.AuthInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; private final AdminInterceptor adminInterceptor; public WebMvcConfig(AuthInterceptor authInterceptor, AdminInterceptor adminInterceptor) { this.authInterceptor = authInterceptor; this.adminInterceptor = adminInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login"); registry.addInterceptor(adminInterceptor) .addPathPatterns("/api/admin/**"); } } ``` - [ ] **Step 5: 验证 Token 功能** Run: `mvn test` Expected: PASS - [ ] **Step 6: Commit** --- ### Task 3: 实体类 + Repository **Files:** - Create: `entity/User.java` - Create: `entity/Chart.java` - Create: `entity/ChatMessage.java` - Create: `entity/PaymentOrder.java` - Create: `entity/Commission.java` - Create: `entity/WithdrawRequest.java` - Create: `entity/SystemConfig.java` - Create: `repository/*Repository.java` - [ ] **Step 1: User.java** ```java package com.num.entity; import jakarta.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, length = 64) private String openId; private String nickName; private String avatarUrl; private String phone; @Column(length = 16) private String tier = "free"; // free / vip private LocalDateTime vipExpireAt; @Column(unique = true, length = 32) private String referralCode; // 推广码 private Long invitedBy; // 上级用户ID private LocalDateTime createTime; private int dailyChartCount; private int dailyChatCount; private int dailyShareCount; private LocalDate lastDailyReset; @PrePersist public void prePersist() { createTime = LocalDateTime.now(); lastDailyReset = LocalDate.now(); } // getters/setters 省略(Lombok @Data 可选) } ``` - [ ] **Step 2: Chart.java** ```java @Entity @Table(name = "charts") public class Chart { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long userId; private String name; private int birthYear; private int birthMonth; private int birthDay; @Column(columnDefinition = "JSON") private String triangleJson; // 15格JSON private int mainCharacter; private boolean isMasterNumber; private LocalDateTime createTime; private boolean deleted; @PrePersist public void prePersist() { createTime = LocalDateTime.now(); } // getters/setters } ``` - [ ] **Step 3: ChatMessage.java** ```java @Entity @Table(name = "chat_messages") public class ChatMessage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long chartId; private Long userId; private String role; // user / assistant @Column(columnDefinition = "TEXT") private String content; private LocalDateTime createTime; @PrePersist public void prePersist() { createTime = LocalDateTime.now(); } // getters/setters } ``` - [ ] **Step 4: PaymentOrder.java** ```java @Entity @Table(name = "payment_orders") public class PaymentOrder { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long userId; private String productType; // vip_yearly private String payStatus; // pending / success / failed / refunded private Integer amount; // 单位:分 private String wxOrderId; private LocalDateTime createTime; private LocalDateTime payTime; // getters/setters } ``` - [ ] **Step 5: Commission.java + WithdrawRequest.java + SystemConfig.java** ```java @Entity @Table(name = "commissions") public class Commission { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long fromUserId; // 消费用户 private Long toUserId; // 获得佣金用户 private int level; // 1=一级 2=二级 private Integer amount; // 分 private Long sourceOrderId; private String status; // available / processing / completed / cancelled private LocalDateTime createTime; private LocalDateTime completeTime; // getters/setters } @Entity @Table(name = "withdraw_requests") public class WithdrawRequest { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long userId; private Integer amount; private String status; // pending / approved / rejected / completed private LocalDateTime createTime; private LocalDateTime auditTime; // getters/setters } @Entity @Table(name = "system_config") public class SystemConfig { @Id private String configKey; @Column(columnDefinition = "TEXT") private String configValue; private String description; private LocalDateTime updateTime; // getters/setters } ``` - [ ] **Step 6: Create Repository 接口** ```java @Repository public interface UserRepository extends JpaRepository { Optional findByOpenId(String openId); Optional findByReferralCode(String referralCode); List findByInvitedBy(Long invitedBy); } @Repository public interface ChartRepository extends JpaRepository { List findByUserIdAndDeletedFalseOrderByCreateTimeDesc(Long userId, Pageable pageable); long countByUserIdAndCreateTimeAfter(Long userId, LocalDateTime after); } @Repository public interface ChatMessageRepository extends JpaRepository { List findByChartIdOrderByCreateTimeAsc(Long chartId); long countByUserIdAndCreateTimeAfter(Long userId, LocalDateTime after); } @Repository public interface PaymentOrderRepository extends JpaRepository { List findByUserIdOrderByCreateTimeDesc(Long userId); Optional findByWxOrderId(String wxOrderId); } @Repository public interface CommissionRepository extends JpaRepository { List findByToUserIdOrderByCreateTimeDesc(Long toUserId); long countByToUserIdAndStatus(Long toUserId, String status); @Query("SELECT COALESCE(SUM(c.amount),0) FROM Commission c WHERE c.toUserId = :userId AND c.status = 'available'") Long sumAvailableByUserId(Long userId); } @Repository public interface WithdrawRequestRepository extends JpaRepository { List findByUserIdOrderByCreateTimeDesc(Long userId); } @Repository public interface SystemConfigRepository extends JpaRepository {} ``` - [ ] **Step 7: 验证编译** Run: `mvn compile` Expected: BUILD SUCCESS - [ ] **Step 8: Commit** --- ### Task 4: CalculatorService — Classic A 算法 **Files:** - Create: `service/CalculatorService.java` - Test: `src/test/java/com/num/service/CalculatorServiceTest.java` - [ ] **Step 1: Create CalculatorService.java** ```java package com.num.service; import org.springframework.stereotype.Service; import java.util.LinkedHashMap; import java.util.Map; @Service public class CalculatorService { public static int reduceToDigit(int n) { if (n == 11 || n == 22 || n == 33) return n; while (n > 9) { int sum = 0; while (n > 0) { sum += n % 10; n /= 10; } if (sum == 11 || sum == 22 || sum == 33) return sum; n = sum; } return n; } public Map calculate(int birthYear, int birthMonth, int birthDay) { String y = String.format("%04d", birthYear); String m = String.format("%02d", birthMonth); String d = String.format("%02d", birthDay); int A = Integer.parseInt(String.valueOf(y.charAt(0))); int B = Integer.parseInt(String.valueOf(y.charAt(1))); int C = reduceToDigit(Integer.parseInt(String.valueOf(y.charAt(2))) + Integer.parseInt(String.valueOf(y.charAt(3)))); int D = reduceToDigit(Integer.parseInt(String.valueOf(m.charAt(0))) + Integer.parseInt(String.valueOf(m.charAt(1)))); int E = reduceToDigit(Integer.parseInt(String.valueOf(d.charAt(0))) + Integer.parseInt(String.valueOf(d.charAt(1)))); // Row 2 int F = reduceToDigit(A + B); int G = reduceToDigit(B + C); int N = reduceToDigit(C + D); int H = reduceToDigit(D + E); // Row 3 int J = reduceToDigit(F + G); int M = reduceToDigit(G + N); int L = reduceToDigit(N + H); // Row 4 int I = reduceToDigit(J + M); int K = reduceToDigit(M + L); // Apex int O = reduceToDigit(I + K); Map positions = new LinkedHashMap<>(); positions.put("A", A); positions.put("B", B); positions.put("C", C); positions.put("D", D); positions.put("E", E); positions.put("F", F); positions.put("G", G); positions.put("N", N); positions.put("H", H); positions.put("J", J); positions.put("M", M); positions.put("L", L); positions.put("I", I); positions.put("K", K); positions.put("O", O); Map zones = buildZones(positions); Map result = new LinkedHashMap<>(); result.put("positions", positions); result.put("zones", zones); result.put("mainCharacter", O); result.put("isMasterNumber", O == 11 || O == 22 || O == 33); return result; } private Map buildZones(Map pos) { Map zones = new LinkedHashMap<>(); zones.put("mainCharacter", zone("主性格", new String[]{"O"}, pos)); zones.put("fatherSource", zone("父源区", new String[]{"I","J"}, pos)); zones.put("motherSource", zone("母源区", new String[]{"K","L"}, pos)); zones.put("leftZone", zone("左区(0-20岁)", new String[]{"A","F","I","O"}, pos)); zones.put("middleZone", zone("中区(20-40岁)", new String[]{"B","G","J","M","N","O"}, pos)); zones.put("rightZone", zone("右区(40-60岁)", new String[]{"C","D","E","H","K","L","O"}, pos)); return zones; } private Map zone(String name, String[] keys, Map pos) { Map z = new LinkedHashMap<>(); z.put("name", name); int[] values = new int[keys.length]; for (int i = 0; i < keys.length; i++) values[i] = pos.get(keys[i]); z.put("positions", java.util.Arrays.asList(keys)); z.put("values", values); return z; } } ``` - [ ] **Step 2: 验证计算** Run: `mvn test -Dtest=CalculatorServiceTest` Expected: PASS - [ ] **Step 3: Commit** --- ### Task 5: ChartService — 能量盘创建+列表 **Files:** - Create: `service/ChartService.java` - Create: `controller/ChartController.java` - Create: `dto/ChartCreateRequest.java` - Create: `dto/ChartListRequest.java` - [ ] **Step 1: ChartService.java** ```java package com.num.service; import com.num.common.BizException; import com.num.entity.Chart; import com.num.repository.ChartRepository; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @Service public class ChartService { private final ChartRepository chartRepository; private final CalculatorService calculator; private final UserService userService; public ChartService(ChartRepository chartRepository, CalculatorService calculator, UserService userService) { this.chartRepository = chartRepository; this.calculator = calculator; this.userService = userService; } public Chart create(Long userId, String name, int year, int month, int day) { // 检查免费用户配额 userService.checkQuota(userId, "chart"); Map calcResult = calculator.calculate(year, month, day); int mainChar = (int) calcResult.get("mainCharacter"); Chart chart = new Chart(); chart.setUserId(userId); chart.setName(name != null ? name : "未知"); chart.setBirthYear(year); chart.setBirthMonth(month); chart.setBirthDay(day); chart.setTriangleJson(new com.google.gson.Gson().toJson(calcResult)); chart.setMainCharacter(mainChar); chart.setMasterNumber((boolean) calcResult.get("isMasterNumber")); chart = chartRepository.save(chart); // 扣减配额 userService.consumeQuota(userId, "chart"); return chart; } public Chart detail(Long id, Long userId) { Chart chart = chartRepository.findById(id).orElseThrow(() -> new BizException(400, "能量盘不存在")); if (!chart.getUserId().equals(userId)) throw new BizException(403, "无权限"); return chart; } public List list(Long userId, int page, int size) { return chartRepository.findByUserIdAndDeletedFalseOrderByCreateTimeDesc(userId, PageRequest.of(page, size)); } public void delete(Long id, Long userId) { Chart chart = detail(id, userId); chart.setDeleted(true); chartRepository.save(chart); } } ``` - [ ] **Step 2: ChartController.java** ```java @RestController @RequestMapping("/api/charts") public class ChartController { @PostMapping("/create") public Result> create(HttpServletRequest req, @RequestBody ChartCreateRequest r) { Long userId = (Long) req.getAttribute("userId"); Chart chart = chartService.create(userId, r.getName(), r.getBirthYear(), r.getBirthMonth(), r.getBirthDay()); return Result.success(Map.of("id", chart.getId(), "mainCharacter", chart.getMainCharacter(), "triangleJson", chart.getTriangleJson(), "createTime", chart.getCreateTime().toString())); } @PostMapping("/detail") public Result detail(HttpServletRequest req, @RequestBody Map body) { return Result.success(chartService.detail(body.get("id"), (Long) req.getAttribute("userId"))); } @PostMapping("/list") public Result> list(HttpServletRequest req, @RequestBody Map body) { return Result.success(chartService.list((Long) req.getAttribute("userId"), body.getOrDefault("page", 0), body.getOrDefault("size", 20))); } @PostMapping("/delete") public Result delete(HttpServletRequest req, @RequestBody Map body) { chartService.delete(body.get("id"), (Long) req.getAttribute("userId")); return Result.success(null); } } ``` - [ ] **Step 3: 验证编译** Run: `mvn compile` Expected: BUILD SUCCESS - [ ] **Step 4: Commit** --- ### Task 6: UserService — 登录 + 配额 + 推广码 **Files:** - Create: `service/UserService.java` - Create: `dto/LoginRequest.java` - Create: `controller/AuthController.java` - [ ] **Step 1: UserService.java** ```java @Service public class UserService { private final UserRepository userRepository; private final JwtConfig jwtConfig; private final RedisTemplate redis; private final ConfigService configService; public Map login(String code, String referrerCode) { // 调微信API获取 openId(简化示例,实际需调用 https://api.weixin.qq.com/sns/jscode2session) String openId = wxCodeToOpenId(code); User user = userRepository.findByOpenId(openId).orElseGet(() -> register(openId, referrerCode)); String token = jwtConfig.generate(user.getId(), user.getTier()); Map data = new LinkedHashMap<>(); data.put("token", token); data.put("user", Map.of("id", user.getId(), "nickName", user.getNickName(), "tier", user.getTier(), "vipExpireAt", user.getVipExpireAt())); return data; } private User register(String openId, String referrerCode) { User user = new User(); user.setOpenId(openId); user.setTier("free"); user.setReferralCode(generateReferralCode()); if (referrerCode != null) { userRepository.findByReferralCode(referrerCode).ifPresent(ref -> user.setInvitedBy(ref.getId())); } return userRepository.save(user); } private String generateReferralCode() { return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(); } public void checkQuota(Long userId, String type) { User user = userRepository.findById(userId).orElseThrow(); if ("vip".equals(user.getTier())) return; resetDailyIfNeeded(user); int max = configService.getQuota(type); int used = type.equals("chart") ? user.getDailyChartCount() : type.equals("chat") ? user.getDailyChatCount() : user.getDailyShareCount(); if (used >= max) throw new BizException(2001, "每日" + type + "次数已达上限"); } public void consumeQuota(Long userId, String type) { User user = userRepository.findById(userId).orElseThrow(); if ("vip".equals(user.getTier())) return; resetDailyIfNeeded(user); if (type.equals("chart")) user.setDailyChartCount(user.getDailyChartCount() + 1); else if (type.equals("chat")) user.setDailyChatCount(user.getDailyChatCount() + 1); else user.setDailyShareCount(user.getDailyShareCount() + 1); userRepository.save(user); } private void resetDailyIfNeeded(User user) { LocalDate today = LocalDate.now(); if (!today.equals(user.getLastDailyReset())) { user.setDailyChartCount(0); user.setDailyChatCount(0); user.setDailyShareCount(0); user.setLastDailyReset(today); } } } ``` - [ ] **Step 2: AuthController.java** ```java @RestController @RequestMapping("/api/auth") public class AuthController { @PostMapping("/login") public Result> login(@RequestBody Map body) { return Result.success(userService.login(body.get("code"), body.get("referrerCode"))); } } ``` - [ ] **Step 3: 验证** Run: `mvn compile` Expected: BUILD SUCCESS - [ ] **Step 4: Commit** --- ### Task 7: ChatService + DifyProxyService **Files:** - Create: `service/DifyProxyService.java` - Create: `service/ChatService.java` - Create: `config/DifyConfig.java` - Create: `controller/ChatController.java` - Create: `dto/ChatSendRequest.java` - [ ] **Step 1: DifyConfig.java** ```java @Configuration public class DifyConfig { @Value("${num.dify.base-url}") private String baseUrl; @Value("${num.dify.api-key}") private String apiKey; public String getBaseUrl() { return baseUrl; } public String getApiKey() { return apiKey; } } ``` - [ ] **Step 2: DifyProxyService.java** ```java @Service public class DifyProxyService { private final DifyConfig difyConfig; private final RestTemplate restTemplate = new RestTemplate(); public String sendMessage(Map chartData, String userMessage, List> history) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + difyConfig.getApiKey()); Map body = new LinkedHashMap<>(); body.put("inputs", Map.of("chart_data", chartData)); body.put("query", userMessage); body.put("response_mode", "blocking"); body.put("conversation_id", ""); HttpEntity> entity = new HttpEntity<>(body, headers); ResponseEntity resp = restTemplate.postForEntity(difyConfig.getBaseUrl() + "/chat-messages", entity, Map.class); Map respBody = resp.getBody(); if (respBody != null && respBody.containsKey("answer")) { return (String) respBody.get("answer"); } throw new BizException(5001, "Dify service error"); } } ``` - [ ] **Step 3: ChatService.java** ```java @Service public class ChatService { private final ChatMessageRepository chatMessageRepository; private final DifyProxyService difyProxy; private final ChartService chartService; private final UserService userService; public String sendMessage(Long userId, Long chartId, String message) { userService.checkQuota(userId, "chat"); // Save user message ChatMessage userMsg = new ChatMessage(); userMsg.setChartId(chartId); userMsg.setUserId(userId); userMsg.setRole("user"); userMsg.setContent(message); chatMessageRepository.save(userMsg); // Load chart data Chart chart = chartService.detail(chartId, userId); Map chartData = new com.google.gson.Gson().fromJson(chart.getTriangleJson(), Map.class); // Load recent history List recent = chatMessageRepository.findByChartIdOrderByCreateTimeAsc(chartId); List> history = new java.util.ArrayList<>(); for (ChatMessage m : recent.subList(Math.max(0, recent.size() - 11), recent.size() - 1)) { history.add(Map.of("role", m.getRole(), "content", m.getContent())); } // Call Dify String reply = difyProxy.sendMessage(chartData, message, history); // Save assistant message ChatMessage aiMsg = new ChatMessage(); aiMsg.setChartId(chartId); aiMsg.setUserId(userId); aiMsg.setRole("assistant"); aiMsg.setContent(reply); chatMessageRepository.save(aiMsg); userService.consumeQuota(userId, "chat"); return reply; } } ``` - [ ] **Step 4: ChatController.java** ```java @RestController @RequestMapping("/api/chat") public class ChatController { @PostMapping("/send") public Result> send(HttpServletRequest req, @RequestBody ChatSendRequest r) { Long userId = (Long) req.getAttribute("userId"); String reply = chatService.sendMessage(userId, r.getChartId(), r.getMessage()); // 返回剩余配额 return Result.success(Map.of("reply", reply)); } @PostMapping("/history") public Result> history(@RequestBody Map body) { return Result.success(chatMessageRepository.findByChartIdOrderByCreateTimeAsc(body.get("chartId"))); } } ``` - [ ] **Step 5: Commit** --- ### Task 8: WxPayService + PaymentController **Files:** - Create: `service/WxPayService.java` - Create: `config/WxPayConfig.java` - Create: `controller/PaymentController.java` - [ ] **Step 1: WxPayConfig.java** ```java @Configuration public class WxPayConfig { @Value("${num.wx.app-id}") private String appId; @Value("${num.wx.mch-id}") private String mchId; @Value("${num.wx.mch-key}") private String mchKey; // getters } ``` - [ ] **Step 2: WxPayService.java(简化,微信支付统一下单)** ```java @Service public class WxPayService { private final WxPayConfig wxPayConfig; private final PaymentOrderRepository orderRepository; private final UserRepository userRepository; private final CommissionService commissionService; public Map unifiedOrder(Long userId, String productType) { int amount = 39800; // ¥398 in 分 String outTradeNo = System.currentTimeMillis() + "_" + userId; PaymentOrder order = new PaymentOrder(); order.setUserId(userId); order.setProductType(productType); order.setPayStatus("pending"); order.setAmount(amount); order.setCreateTime(LocalDateTime.now()); order = orderRepository.save(order); // 实际应调微信支付统一下单API,此处返回模拟参数 Map payParams = new LinkedHashMap<>(); payParams.put("orderId", order.getId()); payParams.put("outTradeNo", outTradeNo); payParams.put("totalFee", amount); return payParams; } @Transactional public void handlePayNotify(String outTradeNo, String wxOrderId) { PaymentOrder order = orderRepository.findByWxOrderId(wxOrderId) .orElseThrow(() -> new BizException(3001, "订单不存在")); order.setPayStatus("success"); order.setPayTime(LocalDateTime.now()); orderRepository.save(order); // 更新用户为VIP User user = userRepository.findById(order.getUserId()).orElseThrow(); user.setTier("vip"); user.setVipExpireAt(LocalDateTime.now().plusYears(1)); userRepository.save(user); // 处理分销佣金 commissionService.handleUpgradeCommission(user.getId(), order.getId(), order.getAmount()); } } ``` - [ ] **Step 3: PaymentController.java** ```java @RestController @RequestMapping("/api/pay") public class PaymentController { @PostMapping("/unified-order") public Result> unifiedOrder(HttpServletRequest req, @RequestBody Map body) { return Result.success(wxPayService.unifiedOrder((Long) req.getAttribute("userId"), body.get("productType"))); } @PostMapping("/notify") // 微信支付回调 public Result payNotify(@RequestBody Map notifyData) { wxPayService.handlePayNotify(notifyData.get("outTradeNo"), notifyData.get("wxOrderId")); return Result.success(null); } } ``` - [ ] **Step 4: Commit** --- ### Task 9: CommissionService + CommissionController **Files:** - Create: `service/CommissionService.java` - Create: `controller/CommissionController.java` - [ ] **Step 1: CommissionService.java** ```java @Service public class CommissionService { private final UserRepository userRepository; private final CommissionRepository commissionRepository; @Transactional public void handleUpgradeCommission(Long userId, Long orderId, int orderAmount) { User user = userRepository.findById(userId).orElseThrow(); Long inviterId = user.getInvitedBy(); if (inviterId == null) return; // 一级佣金 30%(需减去平台固定比例) int level1 = (int) (orderAmount * 0.30); createCommission(userId, inviterId, 1, level1, orderId); // 二级佣金 10% userRepository.findById(inviterId).ifPresent(inviter -> { if (inviter.getInvitedBy() != null) { int level2 = (int) (orderAmount * 0.10); createCommission(userId, inviter.getInvitedBy(), 2, level2, orderId); } }); } private void createCommission(Long fromUserId, Long toUserId, int level, int amount, Long orderId) { Commission c = new Commission(); c.setFromUserId(fromUserId); c.setToUserId(toUserId); c.setLevel(level); c.setAmount(amount); c.setSourceOrderId(orderId); c.setStatus("available"); commissionRepository.save(c); } public Map stats(Long userId) { Long available = commissionRepository.sumAvailableByUserId(userId); List list = commissionRepository.findByToUserIdOrderByCreateTimeDesc(userId); long total = list.stream().mapToLong(c -> c.getAmount()).sum(); Map stats = new LinkedHashMap<>(); stats.put("totalEarnings", total); stats.put("available", available != null ? available : 0); stats.put("count", list.size()); return stats; } } ``` - [ ] **Step 2: CommissionController.java** ```java @RestController @RequestMapping("/api/commission") public class CommissionController { @PostMapping("/stats") public Result> stats(HttpServletRequest req) { return Result.success(commissionService.stats((Long) req.getAttribute("userId"))); } @PostMapping("/list") public Result> list(HttpServletRequest req, @RequestBody Map body) { Long userId = (Long) req.getAttribute("userId"); List list = commissionRepository.findByToUserIdOrderByCreateTimeDesc(userId); return Result.success(list); } } @RestController @RequestMapping("/api/withdraw") public class WithdrawController { @PostMapping("/create") public Result create(HttpServletRequest req, @RequestBody Map body) { Long userId = (Long) req.getAttribute("userId"); int amount = body.get("amount"); if (amount < 10000) throw new BizException(400, "最低提现 ¥100"); Long available = commissionRepository.sumAvailableByUserId(userId); if (available == null || available < amount) throw new BizException(400, "可提现金额不足"); WithdrawRequest wr = new WithdrawRequest(); wr.setUserId(userId); wr.setAmount(amount); wr.setStatus("pending"); withdrawRequestRepository.save(wr); return Result.success(null); } @PostMapping("/list") public Result> list(HttpServletRequest req) { return Result.success(withdrawRequestRepository.findByUserIdOrderByCreateTimeDesc((Long) req.getAttribute("userId"))); } } ``` - [ ] **Step 3: Commit** --- ### Task 10: AdminController — 管理端 API **Files:** - Create: `controller/AdminController.java` - Create: `service/ConfigService.java` - [ ] **Step 1: ConfigService.java** ```java @Service public class ConfigService { private final SystemConfigRepository configRepository; private static final Map DEFAULT_QUOTA = Map.of("chart", 3, "chat", 5, "share", 1); public int getQuota(String type) { return configRepository.findById("quota_" + type) .map(c -> Integer.parseInt(c.getConfigValue())) .orElse(DEFAULT_QUOTA.getOrDefault(type, 3)); } public Map getAll() { return configRepository.findAll().stream() .collect(Collectors.toMap(SystemConfig::getConfigKey, SystemConfig::getConfigValue)); } public void update(String key, String value) { SystemConfig config = configRepository.findById(key).orElse(new SystemConfig()); config.setConfigKey(key); config.setConfigValue(value); config.setUpdateTime(LocalDateTime.now()); configRepository.save(config); } } ``` - [ ] **Step 2: 验证编译 + 启动测试** Run: `mvn clean compile` Expected: BUILD SUCCESS - [ ] **Step 3: Commit**