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
Files:
pom.xmlNumApplication.javacommon/Result.javacommon/BizException.javacommon/GlobalExceptionHandler.javaapplication.ymlCreate: schema.sql
[ ] Step 1: Create pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
</parent>
<groupId>com.num</groupId>
<artifactId>num-server</artifactId>
<version>1.0.0</version>
<name>num-server</name>
<description>Digital Energy Numerology Backend</description>
<properties><java.version>17</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.12.3</version></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.12.3</version><scope>runtime</scope></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.12.3</version><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.10.1</version></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
[ ] Step 2: Create application.yml
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
package com.num.common;
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 0; r.message = "success"; r.data = data;
return r;
}
public static <T> Result<T> error(int code, String message) {
Result<T> r = new Result<>();
r.code = code; r.message = message;
return r;
}
// getters/setters
}
[ ] Step 4: Create BizException.java + GlobalExceptionHandler.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; }
}
@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)
CREATE DATABASE IF NOT EXISTS num_db DEFAULT CHARSET utf8mb4;
[ ] Step 6: 验证启动
Run: mvn spring-boot:run
Expected: 启动成功,无报错,访问 http://localhost:8080/ 返回 404(正常)
Files:
config/JwtConfig.javaconfig/WebMvcConfig.javainterceptor/AuthInterceptor.javaCreate: interceptor/AdminInterceptor.java
[ ] Step 1: Create JwtConfig.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
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
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
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
Files:
entity/User.javaentity/Chart.javaentity/ChatMessage.javaentity/PaymentOrder.javaentity/Commission.javaentity/WithdrawRequest.javaentity/SystemConfig.javaCreate: repository/*Repository.java
[ ] Step 1: User.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
@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
@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
@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
@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 接口
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByOpenId(String openId);
Optional<User> findByReferralCode(String referralCode);
List<User> findByInvitedBy(Long invitedBy);
}
@Repository
public interface ChartRepository extends JpaRepository<Chart, Long> {
List<Chart> findByUserIdAndDeletedFalseOrderByCreateTimeDesc(Long userId, Pageable pageable);
long countByUserIdAndCreateTimeAfter(Long userId, LocalDateTime after);
}
@Repository
public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {
List<ChatMessage> findByChartIdOrderByCreateTimeAsc(Long chartId);
long countByUserIdAndCreateTimeAfter(Long userId, LocalDateTime after);
}
@Repository
public interface PaymentOrderRepository extends JpaRepository<PaymentOrder, Long> {
List<PaymentOrder> findByUserIdOrderByCreateTimeDesc(Long userId);
Optional<PaymentOrder> findByWxOrderId(String wxOrderId);
}
@Repository
public interface CommissionRepository extends JpaRepository<Commission, Long> {
List<Commission> 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<WithdrawRequest, Long> {
List<WithdrawRequest> findByUserIdOrderByCreateTimeDesc(Long userId);
}
@Repository
public interface SystemConfigRepository extends JpaRepository<SystemConfig, String> {}
[ ] Step 7: 验证编译
Run: mvn compile
Expected: BUILD SUCCESS
Files:
service/CalculatorService.javaTest: src/test/java/com/num/service/CalculatorServiceTest.java
[ ] Step 1: Create CalculatorService.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<String, Object> 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<String, Integer> 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<String, Object> zones = buildZones(positions);
Map<String, Object> 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<String, Object> buildZones(Map<String, Integer> pos) {
Map<String, Object> 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<String, Object> zone(String name, String[] keys, Map<String, Integer> pos) {
Map<String, Object> 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
Files:
service/ChartService.javacontroller/ChartController.javadto/ChartCreateRequest.javaCreate: dto/ChartListRequest.java
[ ] Step 1: ChartService.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<String, Object> 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<Chart> 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
@RestController @RequestMapping("/api/charts")
public class ChartController {
@PostMapping("/create")
public Result<Map<String, Object>> 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<Chart> detail(HttpServletRequest req, @RequestBody Map<String, Long> body) {
return Result.success(chartService.detail(body.get("id"), (Long) req.getAttribute("userId")));
}
@PostMapping("/list")
public Result<List<Chart>> list(HttpServletRequest req, @RequestBody Map<String, Integer> body) {
return Result.success(chartService.list((Long) req.getAttribute("userId"),
body.getOrDefault("page", 0), body.getOrDefault("size", 20)));
}
@PostMapping("/delete")
public Result<Void> delete(HttpServletRequest req, @RequestBody Map<String, Long> body) {
chartService.delete(body.get("id"), (Long) req.getAttribute("userId"));
return Result.success(null);
}
}
[ ] Step 3: 验证编译
Run: mvn compile
Expected: BUILD SUCCESS
Files:
service/UserService.javadto/LoginRequest.javaCreate: controller/AuthController.java
[ ] Step 1: UserService.java
@Service
public class UserService {
private final UserRepository userRepository;
private final JwtConfig jwtConfig;
private final RedisTemplate<String, Integer> redis;
private final ConfigService configService;
public Map<String, Object> 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<String, Object> 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
@RestController @RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody Map<String, String> body) {
return Result.success(userService.login(body.get("code"), body.get("referrerCode")));
}
}
[ ] Step 3: 验证
Run: mvn compile
Expected: BUILD SUCCESS
Files:
service/DifyProxyService.javaservice/ChatService.javaconfig/DifyConfig.javacontroller/ChatController.javaCreate: dto/ChatSendRequest.java
[ ] Step 1: DifyConfig.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
@Service
public class DifyProxyService {
private final DifyConfig difyConfig;
private final RestTemplate restTemplate = new RestTemplate();
public String sendMessage(Map<String, Object> chartData, String userMessage, List<Map<String, String>> history) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + difyConfig.getApiKey());
Map<String, Object> 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<Map<String, Object>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> resp = restTemplate.postForEntity(difyConfig.getBaseUrl() + "/chat-messages", entity, Map.class);
Map<String, Object> 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
@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<String, Object> chartData = new com.google.gson.Gson().fromJson(chart.getTriangleJson(), Map.class);
// Load recent history
List<ChatMessage> recent = chatMessageRepository.findByChartIdOrderByCreateTimeAsc(chartId);
List<Map<String, String>> 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
@RestController @RequestMapping("/api/chat")
public class ChatController {
@PostMapping("/send")
public Result<Map<String, Object>> 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<List<ChatMessage>> history(@RequestBody Map<String, Long> body) {
return Result.success(chatMessageRepository.findByChartIdOrderByCreateTimeAsc(body.get("chartId")));
}
}
[ ] Step 5: Commit
Files:
service/WxPayService.javaconfig/WxPayConfig.javaCreate: controller/PaymentController.java
[ ] Step 1: WxPayConfig.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(简化,微信支付统一下单)
@Service
public class WxPayService {
private final WxPayConfig wxPayConfig;
private final PaymentOrderRepository orderRepository;
private final UserRepository userRepository;
private final CommissionService commissionService;
public Map<String, Object> 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<String, Object> 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
@RestController @RequestMapping("/api/pay")
public class PaymentController {
@PostMapping("/unified-order")
public Result<Map<String, Object>> unifiedOrder(HttpServletRequest req, @RequestBody Map<String, String> body) {
return Result.success(wxPayService.unifiedOrder((Long) req.getAttribute("userId"), body.get("productType")));
}
@PostMapping("/notify") // 微信支付回调
public Result<Void> payNotify(@RequestBody Map<String, String> notifyData) {
wxPayService.handlePayNotify(notifyData.get("outTradeNo"), notifyData.get("wxOrderId"));
return Result.success(null);
}
}
[ ] Step 4: Commit
Files:
service/CommissionService.javaCreate: controller/CommissionController.java
[ ] Step 1: CommissionService.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<String, Object> stats(Long userId) {
Long available = commissionRepository.sumAvailableByUserId(userId);
List<Commission> list = commissionRepository.findByToUserIdOrderByCreateTimeDesc(userId);
long total = list.stream().mapToLong(c -> c.getAmount()).sum();
Map<String, Object> 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
@RestController @RequestMapping("/api/commission")
public class CommissionController {
@PostMapping("/stats")
public Result<Map<String, Object>> stats(HttpServletRequest req) {
return Result.success(commissionService.stats((Long) req.getAttribute("userId")));
}
@PostMapping("/list")
public Result<List<Commission>> list(HttpServletRequest req, @RequestBody Map<String, Integer> body) {
Long userId = (Long) req.getAttribute("userId");
List<Commission> list = commissionRepository.findByToUserIdOrderByCreateTimeDesc(userId);
return Result.success(list);
}
}
@RestController @RequestMapping("/api/withdraw")
public class WithdrawController {
@PostMapping("/create")
public Result<Void> create(HttpServletRequest req, @RequestBody Map<String, Integer> 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<WithdrawRequest>> list(HttpServletRequest req) {
return Result.success(withdrawRequestRepository.findByUserIdOrderByCreateTimeDesc((Long) req.getAttribute("userId")));
}
}
[ ] Step 3: Commit
Files:
controller/AdminController.javaCreate: service/ConfigService.java
[ ] Step 1: ConfigService.java
@Service
public class ConfigService {
private final SystemConfigRepository configRepository;
private static final Map<String, Integer> 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<String, String> 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