Initial commit: Monisuo - 虚拟货币模拟交易平台

功能模块:
- 用户注册/登录/KYC
- 资金账户/交易账户
- 实时行情/币种管理
- 即时交易/充提审核
- 管理后台

技术栈:
- 后端: SpringBoot 2.2.4 + MyBatis Plus
- 前端: uni-app x (Vue3 + UTS)
- 数据库: MySQL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sion
2026-03-21 20:52:33 +08:00
commit 7694a34ade
108 changed files with 12563 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
package com.it.rattan;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ServletComponentScan(basePackages ={"com.it.rattan"})
@ComponentScan(basePackages ={"com.it.rattan"})
/*@EnableAsync
@EnableAspectJAutoProxy*/
public class SpcCloudApplication {
public static void main(String[] args) {
SpringApplication.run(SpcCloudApplication.class, args);
}
}

View File

@@ -0,0 +1,62 @@
package com.it.rattan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).
useDefaultResponseMessages(false)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.regex("^(?!auth).*$"))
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
;
}
private List<ApiKey> securitySchemes() {
List<ApiKey> apiKey = new ArrayList();
apiKey.add(new ApiKey("Authorization", "Authorization", "header"));
return apiKey;
}
private List<SecurityContext> securityContexts() {
List<SecurityContext> securityContexts = new ArrayList();
SecurityContext securityContext = SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("^(?!auth).*$"))
.build();
securityContexts.add(securityContext);
return securityContexts;
}
List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferences = new ArrayList();
SecurityReference securityContext = new SecurityReference("Authorization", authorizationScopes);
securityReferences.add(securityContext);
return securityReferences;
}
}

View File

@@ -0,0 +1,13 @@
/*
package com.it.rattan.config;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.boot.autoconfigure.SpringBootApplication;
public class RattanClientConfig {
}
*/

View File

@@ -0,0 +1,40 @@
package com.it.rattan.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许所有来源
config.addAllowedOrigin("*");
// 允许所有请求头
config.addAllowedHeader("*");
// 允许所有请求方法
config.addAllowedMethod("*");
// 允许携带凭证
config.setAllowCredentials(true);
// 预检请求缓存时间
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,9 @@
package com.it.rattan.enums;
public interface RattanMark {
String EMPTY = "";
String NULL = null;
}

View File

@@ -0,0 +1,17 @@
package com.it.rattan.exception;
import com.it.rattan.rpc.RattanResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object handle(final Exception e){
return RattanResponse.fail(e.getMessage());
}
}

View File

@@ -0,0 +1,21 @@
package com.it.rattan.exception;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LoginException extends RuntimeException {
private static final String CODE = "401";
private String msg;
private String code;
public LoginException(String msg){
super(msg);
this.code = CODE;
}
}

View File

@@ -0,0 +1,103 @@
package com.it.rattan.intceptor;
import com.ejlchina.searcher.SearchSql;
import com.ejlchina.searcher.SqlInterceptor;
import com.ejlchina.searcher.util.StringUtils;
import com.it.rattan.SpcCloudApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class MySqlInterceptor implements SqlInterceptor {
@Override
public <T> SearchSql<T> intercept(SearchSql<T> searchSql, Map<String, Object> paraMap) {
if (searchSql.isShouldQueryList()) {
String listSql = searchSql.getListSqlString();
String countSql = searchSql.getClusterSqlString();
String prodSearchKey = (String) paraMap.get("prodName-prodModel");
String orderByStr = "";
if(listSql.toUpperCase().contains(" ORDER BY ")){
orderByStr = listSql.substring(listSql.toUpperCase().indexOf("ORDER BY"));
orderByStr = orderByStr.substring(0,orderByStr.indexOf("limit"));
}
if(StringUtils.isNotBlank(prodSearchKey)){
listSql = listSql.replace(orderByStr,"");
if(listSql.toUpperCase().contains(" WHERE ")){
listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") );
countSql = countSql + "and (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%')";
}else{
listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit"));
countSql = countSql + " where (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%')";
}
}
String quotationSearchKey = (String) paraMap.get("projName-quotationNum");
if(StringUtils.isNotBlank(quotationSearchKey)){
listSql = listSql.replace(orderByStr,"");
if(listSql.toUpperCase().contains(" WHERE ")){
listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") );
countSql = countSql + "and (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%')";
}else{
listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit"));
countSql = countSql + " where (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%')";
}
}
String contractSearchKey = (String) paraMap.get("contractNum-contractName");
if(StringUtils.isNotBlank(contractSearchKey)){
listSql = listSql.replace(orderByStr,"");
if(listSql.toUpperCase().contains(" WHERE ")){
listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") );
countSql = countSql + "and (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%')";
}else{
listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit"));
countSql = countSql + " where (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%')";
}
}
String userSearchKey = (String) paraMap.get("userAccount-userName");
if(StringUtils.isNotBlank(userSearchKey)){
listSql = listSql.replace(orderByStr,"");
if(listSql.toUpperCase().contains(" WHERE ")){
listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") );
countSql = countSql + "and (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%')";
}else{
listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit"));
countSql = countSql + " where (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%')";
}
}
String searchDay = (String) paraMap.get("searchDay");
//YEARWEEK( FROM_UNIXTIME( `created_at`, "%Y-%m-%d %H:%i:%s" ) ,1) = YEARWEEK( now(),1 )
if(StringUtils.isNotBlank(searchDay) && ("today".equals(searchDay) || "week".equals(searchDay))){
listSql = listSql.replace(orderByStr,"");
if(listSql.toUpperCase().contains(" WHERE ")){
listSql = listSql.substring(0,listSql.indexOf("limit")) + "and ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+") " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") );
countSql = countSql + "and ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+")";
}else{
listSql = listSql.substring(0,listSql.indexOf("limit")) + "where ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+") " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit"));
countSql = countSql + " where ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+")";
}
}
searchSql.setListSqlString(listSql);
searchSql.setClusterSqlString(countSql);
}
return searchSql;
}
public static void main(String[] args) {
String aa = "adasdhask order by as desc limt 5,5";
String bb = aa.substring(aa.indexOf("order by"));
System.out.println(bb);
}
}

View File

@@ -0,0 +1,40 @@
package com.it.rattan.monisuo.common;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果
*/
@Data
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/** 数据列表 */
private List<T> list;
/** 总数 */
private long total;
/** 当前页 */
private long pageNum;
/** 每页数量 */
private long pageSize;
/** 总页数 */
private long pages;
public PageResult() {
}
public PageResult(List<T> list, long total, long pageNum, long pageSize) {
this.list = list;
this.total = total;
this.pageNum = pageNum;
this.pageSize = pageSize;
this.pages = (total + pageSize - 1) / pageSize;
}
public static <T> PageResult<T> of(List<T> list, long total, long pageNum, long pageSize) {
return new PageResult<>(list, total, pageNum, pageSize);
}
}

View File

@@ -0,0 +1,64 @@
package com.it.rattan.monisuo.common;
import lombok.Data;
import java.io.Serializable;
/**
* 统一响应结果
*/
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/** 成功状态码 */
public static final String SUCCESS_CODE = "0000";
/** 失败状态码 */
public static final String FAIL_CODE = "0001";
/** 未授权状态码 */
public static final String UNAUTHORIZED_CODE = "0002";
/** 状态码 */
private String code;
/** 消息 */
private String msg;
/** 数据 */
private T data;
public Result() {
}
public Result(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> Result<T> success() {
return new Result<>(SUCCESS_CODE, "操作成功", null);
}
public static <T> Result<T> success(T data) {
return new Result<>(SUCCESS_CODE, "操作成功", data);
}
public static <T> Result<T> success(String msg, T data) {
return new Result<>(SUCCESS_CODE, msg, data);
}
public static <T> Result<T> fail(String msg) {
return new Result<>(FAIL_CODE, msg, null);
}
public static <T> Result<T> fail(String code, String msg) {
return new Result<>(code, msg, null);
}
public static <T> Result<T> unauthorized(String msg) {
return new Result<>(UNAUTHORIZED_CODE, msg, null);
}
public boolean isSuccess() {
return SUCCESS_CODE.equals(this.code);
}
}

View File

@@ -0,0 +1,46 @@
package com.it.rattan.monisuo.context;
import lombok.Data;
/**
* 用户上下文信息
*/
@Data
public class UserContext {
/** 用户ID */
private Long userId;
/** 用户名 */
private String username;
/** 类型: user/admin */
private String type;
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
public static void set(UserContext context) {
CONTEXT.set(context);
}
public static UserContext get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
public static Long getUserId() {
UserContext context = get();
return context != null ? context.getUserId() : null;
}
public static String getUsername() {
UserContext context = get();
return context != null ? context.getUsername() : null;
}
public static boolean isAdmin() {
UserContext context = get();
return context != null && "admin".equals(context.getType());
}
}

View File

@@ -0,0 +1,359 @@
package com.it.rattan.monisuo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.entity.*;
import com.it.rattan.monisuo.mapper.AccountFundMapper;
import com.it.rattan.monisuo.mapper.OrderFundMapper;
import com.it.rattan.monisuo.service.*;
import com.it.rattan.monisuo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 管理后台接口
*/
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private UserService userService;
@Autowired
private CoinService coinService;
@Autowired
private FundService fundService;
@Autowired
private AssetService assetService;
@Autowired
private AccountFundMapper accountFundMapper;
@Autowired
private OrderFundMapper orderFundMapper;
/**
* 管理员登录
*/
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody Map<String, String> params) {
String username = params.get("username");
String password = params.get("password");
if (username == null || password == null) {
return Result.fail("用户名和密码不能为空");
}
// 简单验证,实际应从数据库查询
// 这里预置账号: admin/admin123, superadmin/admin123
String validPassword = "$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9vz.a"; // admin123
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder encoder =
new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder();
// 临时处理:预置账号
if (("admin".equals(username) || "superadmin".equals(username)) &&
encoder.matches(password, validPassword)) {
String token = JwtUtil.createToken(1L, username, "admin");
Map<String, Object> result = new HashMap<>();
result.put("token", token);
Map<String, Object> adminInfo = new HashMap<>();
adminInfo.put("id", 1L);
adminInfo.put("username", username);
adminInfo.put("nickname", "超级管理员");
adminInfo.put("role", 1);
result.put("adminInfo", adminInfo);
return Result.success("登录成功", result);
}
return Result.fail("用户名或密码错误");
}
/**
* 用户列表
*/
@GetMapping("/user/list")
public Result<Map<String, Object>> getUserList(
@RequestParam(required = false) String username,
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (username != null && !username.isEmpty()) {
wrapper.like(User::getUsername, username);
}
if (status != null) {
wrapper.eq(User::getStatus, status);
}
wrapper.orderByDesc(User::getCreateTime);
Page<User> page = new Page<>(pageNum, pageSize);
IPage<User> result = userService.page(page, wrapper);
Map<String, Object> data = new HashMap<>();
data.put("list", result.getRecords());
data.put("total", result.getTotal());
data.put("pageNum", result.getCurrent());
data.put("pageSize", result.getSize());
return Result.success(data);
}
/**
* 用户详情
*/
@GetMapping("/user/detail")
public Result<Map<String, Object>> getUserDetail(@RequestParam Long userId) {
User user = userService.getById(userId);
if (user == null) {
return Result.fail("用户不存在");
}
Map<String, Object> data = new HashMap<>();
data.put("user", user);
// 资产信息
AccountFund fund = assetService.getOrCreateFundAccount(userId);
data.put("fund", fund);
// 交易账户
List<Map<String, Object>> trade = assetService.getTradeAccount(userId);
data.put("trade", trade);
return Result.success(data);
}
/**
* 禁用/启用用户
*/
@PostMapping("/user/status")
public Result<Void> updateUserStatus(@RequestBody Map<String, Object> params) {
Long userId = Long.valueOf(params.get("userId").toString());
Integer status = (Integer) params.get("status");
User user = userService.getById(userId);
if (user == null) {
return Result.fail("用户不存在");
}
user.setStatus(status);
user.setUpdateTime(LocalDateTime.now());
userService.updateById(user);
return Result.success(status == 1 ? "已启用" : "已禁用", null);
}
/**
* 币种列表
*/
@GetMapping("/coin/list")
public Result<Map<String, Object>> getCoinList() {
List<Coin> coins = coinService.list();
Map<String, Object> data = new HashMap<>();
data.put("list", coins);
return Result.success(data);
}
/**
* 新增/编辑币种
*/
@PostMapping("/coin/save")
public Result<Void> saveCoin(@RequestBody Coin coin) {
if (coin.getCode() == null || coin.getCode().isEmpty()) {
return Result.fail("币种代码不能为空");
}
if (coin.getName() == null || coin.getName().isEmpty()) {
return Result.fail("币种名称不能为空");
}
coin.setCode(coin.getCode().toUpperCase());
if (coin.getId() == null) {
// 新增
coin.setCreateTime(LocalDateTime.now());
coinService.save(coin);
} else {
// 编辑
coin.setUpdateTime(LocalDateTime.now());
coinService.updateById(coin);
}
return Result.success("保存成功", null);
}
/**
* 调整币种价格
*/
@PostMapping("/coin/price")
public Result<Void> updateCoinPrice(@RequestBody Map<String, Object> params) {
String code = (String) params.get("code");
BigDecimal price = new BigDecimal(params.get("price").toString());
Coin coin = coinService.getCoinByCode(code);
if (coin == null) {
return Result.fail("币种不存在");
}
if (coin.getPriceType() == 1) {
return Result.fail("实时币种价格不可手动修改");
}
coin.setPrice(price);
coin.setUpdateTime(LocalDateTime.now());
coinService.updateById(coin);
return Result.success("价格已更新", null);
}
/**
* 币种上架/下架
*/
@PostMapping("/coin/status")
public Result<Void> updateCoinStatus(@RequestBody Map<String, Object> params) {
Long coinId = Long.valueOf(params.get("coinId").toString());
Integer status = (Integer) params.get("status");
Coin coin = coinService.getById(coinId);
if (coin == null) {
return Result.fail("币种不存在");
}
coin.setStatus(status);
coin.setUpdateTime(LocalDateTime.now());
coinService.updateById(coin);
return Result.success(status == 1 ? "已上架" : "已下架", null);
}
/**
* 价格计算器
*/
@PostMapping("/coin/calculator")
public Result<Map<String, Object>> calculatePrice(@RequestBody Map<String, Object> params) {
BigDecimal currentPrice = new BigDecimal(params.get("currentPrice").toString());
BigDecimal holdingAmount = new BigDecimal(params.getOrDefault("holdingAmount", "1000").toString());
BigDecimal targetProfit = new BigDecimal(params.get("targetProfit").toString());
// 计算目标价格
// 持仓数量 = 持仓金额 / 当前价格
BigDecimal quantity = holdingAmount.divide(currentPrice, 8, BigDecimal.ROUND_DOWN);
// 单币盈亏 = 目标盈亏 / 持仓数量
BigDecimal profitPerCoin = targetProfit.divide(quantity, 8, BigDecimal.ROUND_DOWN);
// 目标价格 = 当前价格 + 单币盈亏
BigDecimal targetPrice = currentPrice.add(profitPerCoin);
Map<String, Object> result = new HashMap<>();
result.put("targetPrice", targetPrice);
result.put("quantity", quantity);
result.put("profitPerCoin", profitPerCoin);
return Result.success(result);
}
/**
* 待审批订单
*/
@GetMapping("/order/pending")
public Result<Map<String, Object>> getPendingOrders(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
IPage<OrderFund> page = fundService.getPendingOrders(pageNum, pageSize);
Map<String, Object> data = new HashMap<>();
data.put("list", page.getRecords());
data.put("total", page.getTotal());
return Result.success(data);
}
/**
* 所有充提订单
*/
@GetMapping("/order/list")
public Result<Map<String, Object>> getAllOrders(
@RequestParam(required = false) Integer type,
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
IPage<OrderFund> page = fundService.getAllOrders(type, status, pageNum, pageSize);
Map<String, Object> data = new HashMap<>();
data.put("list", page.getRecords());
data.put("total", page.getTotal());
data.put("pageNum", page.getCurrent());
data.put("pageSize", page.getSize());
return Result.success(data);
}
/**
* 审批订单
*/
@PostMapping("/order/approve")
public Result<Void> approveOrder(@RequestBody Map<String, Object> params) {
String orderNo = (String) params.get("orderNo");
Integer status = (Integer) params.get("status");
String rejectReason = (String) params.get("rejectReason");
String adminRemark = (String) params.get("adminRemark");
if (orderNo == null || status == null) {
return Result.fail("参数错误");
}
if (status != 2 && status != 3) {
return Result.fail("状态参数错误");
}
try {
fundService.approve(1L, "管理员", orderNo, status, rejectReason, adminRemark);
return Result.success(status == 2 ? "审批通过" : "已驳回", null);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 资金总览
*/
@GetMapping("/finance/overview")
public Result<Map<String, Object>> getFinanceOverview() {
Map<String, Object> data = new HashMap<>();
// 累计充值
BigDecimal totalDeposit = orderFundMapper.sumCompletedDeposit();
data.put("totalDeposit", totalDeposit);
// 累计提现
BigDecimal totalWithdraw = orderFundMapper.sumCompletedWithdraw();
data.put("totalWithdraw", totalWithdraw);
// 在管资金
BigDecimal fundBalance = accountFundMapper.sumAllBalance();
data.put("fundBalance", fundBalance);
// 交易账户总值
BigDecimal tradeValue = accountFundMapper.sumAllTradeValue();
data.put("tradeValue", tradeValue != null ? tradeValue : BigDecimal.ZERO);
// 待审批数量
int pendingCount = orderFundMapper.countPending();
data.put("pendingCount", pendingCount);
// 用户总数
long userCount = userService.count();
data.put("userCount", userCount);
return Result.success(data);
}
}

View File

@@ -0,0 +1,129 @@
package com.it.rattan.monisuo.controller;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.service.AssetService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 资产接口
*/
@RestController
@RequestMapping("/api/asset")
public class AssetController {
@Autowired
private AssetService assetService;
/**
* 资产总览
*/
@GetMapping("/overview")
public Result<Map<String, Object>> getOverview() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
Map<String, Object> result = assetService.getOverview(userId);
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 资金账户
*/
@GetMapping("/fund")
public Result<Map<String, Object>> getFundAccount() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
Map<String, Object> result = new java.util.HashMap<>();
result.put("fund", assetService.getOrCreateFundAccount(userId));
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 交易账户
*/
@GetMapping("/trade")
public Result<Map<String, Object>> getTradeAccount() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
List<Map<String, Object>> positions = assetService.getTradeAccount(userId);
Map<String, Object> result = new java.util.HashMap<>();
result.put("positions", positions);
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 资金划转
*/
@PostMapping("/transfer")
public Result<Void> transfer(@RequestBody Map<String, Object> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
Integer direction = (Integer) params.get("direction");
BigDecimal amount = new BigDecimal(params.get("amount").toString());
if (direction == null || (direction != 1 && direction != 2)) {
return Result.fail("请选择划转方向");
}
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("划转金额必须大于0");
}
try {
assetService.transfer(userId, direction, amount);
return Result.success("划转成功", null);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 资金流水
*/
@GetMapping("/flow")
public Result<Map<String, Object>> getFlows(
@RequestParam(required = false) Integer flowType,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
List<?> flows = assetService.getFlows(userId, flowType, pageNum, pageSize);
Map<String, Object> result = new java.util.HashMap<>();
result.put("list", flows);
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,121 @@
package com.it.rattan.monisuo.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.entity.OrderFund;
import com.it.rattan.monisuo.service.FundService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Map;
/**
* 充提接口
*/
@RestController
@RequestMapping("/api/fund")
public class FundController {
@Autowired
private FundService fundService;
/**
* 申请充值
*/
@PostMapping("/deposit")
public Result<Map<String, Object>> deposit(@RequestBody Map<String, Object> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
BigDecimal amount = new BigDecimal(params.get("amount").toString());
String remark = (String) params.get("remark");
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("充值金额必须大于0");
}
try {
Map<String, Object> result = fundService.deposit(userId, amount, remark);
return Result.success("申请成功,等待审批", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 申请提现
*/
@PostMapping("/withdraw")
public Result<Map<String, Object>> withdraw(@RequestBody Map<String, Object> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
BigDecimal amount = new BigDecimal(params.get("amount").toString());
String remark = (String) params.get("remark");
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("提现金额必须大于0");
}
try {
Map<String, Object> result = fundService.withdraw(userId, amount, remark);
return Result.success("申请成功,等待审批", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 取消订单
*/
@PostMapping("/cancel")
public Result<Void> cancel(@RequestBody Map<String, String> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
String orderNo = params.get("orderNo");
if (orderNo == null || orderNo.isEmpty()) {
return Result.fail("订单号不能为空");
}
try {
fundService.cancel(userId, orderNo);
return Result.success("取消成功", null);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 充提记录
*/
@GetMapping("/orders")
public Result<Map<String, Object>> getOrders(
@RequestParam(required = false) Integer type,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
IPage<OrderFund> page = fundService.getOrders(userId, type, pageNum, pageSize);
Map<String, Object> result = new java.util.HashMap<>();
result.put("list", page.getRecords());
result.put("total", page.getTotal());
result.put("pageNum", page.getCurrent());
result.put("pageSize", page.getSize());
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,74 @@
package com.it.rattan.monisuo.controller;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.entity.Coin;
import com.it.rattan.monisuo.service.CoinService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 行情接口
*/
@RestController
@RequestMapping("/api/market")
public class MarketController {
@Autowired
private CoinService coinService;
/**
* 币种列表
*/
@GetMapping("/list")
public Result<Map<String, Object>> getCoinList() {
try {
List<Coin> coins = coinService.getActiveCoins();
Map<String, Object> result = new HashMap<>();
result.put("list", coins);
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 币种详情
*/
@GetMapping("/detail")
public Result<Coin> getCoinDetail(@RequestParam String code) {
try {
Coin coin = coinService.getCoinByCode(code);
if (coin == null) {
return Result.fail("币种不存在");
}
return Result.success(coin);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 搜索币种
*/
@GetMapping("/search")
public Result<Map<String, Object>> searchCoins(@RequestParam String keyword) {
try {
List<Coin> coins = coinService.getActiveCoins();
List<Coin> filtered = new java.util.ArrayList<>();
for (Coin coin : coins) {
if (coin.getCode().toLowerCase().contains(keyword.toLowerCase()) ||
coin.getName().toLowerCase().contains(keyword.toLowerCase())) {
filtered.add(coin);
}
}
Map<String, Object> result = new HashMap<>();
result.put("list", filtered);
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,134 @@
package com.it.rattan.monisuo.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.entity.OrderTrade;
import com.it.rattan.monisuo.service.TradeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Map;
/**
* 交易接口
*/
@RestController
@RequestMapping("/api/trade")
public class TradeController {
@Autowired
private TradeService tradeService;
/**
* 买入
*/
@PostMapping("/buy")
public Result<Map<String, Object>> buy(@RequestBody Map<String, Object> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
String coinCode = (String) params.get("coinCode");
BigDecimal price = new BigDecimal(params.get("price").toString());
BigDecimal quantity = new BigDecimal(params.get("quantity").toString());
if (coinCode == null || coinCode.isEmpty()) {
return Result.fail("请选择币种");
}
if (price.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("价格必须大于0");
}
if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("数量必须大于0");
}
try {
Map<String, Object> result = tradeService.buy(userId, coinCode.toUpperCase(), price, quantity);
return Result.success("买入成功", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 卖出
*/
@PostMapping("/sell")
public Result<Map<String, Object>> sell(@RequestBody Map<String, Object> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
String coinCode = (String) params.get("coinCode");
BigDecimal price = new BigDecimal(params.get("price").toString());
BigDecimal quantity = new BigDecimal(params.get("quantity").toString());
if (coinCode == null || coinCode.isEmpty()) {
return Result.fail("请选择币种");
}
if (price.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("价格必须大于0");
}
if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("数量必须大于0");
}
try {
Map<String, Object> result = tradeService.sell(userId, coinCode.toUpperCase(), price, quantity);
return Result.success("卖出成功", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 交易记录
*/
@GetMapping("/orders")
public Result<Map<String, Object>> getOrders(
@RequestParam(required = false) String coinCode,
@RequestParam(required = false) Integer direction,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
IPage<OrderTrade> page = tradeService.getOrders(userId, coinCode, direction, pageNum, pageSize);
Map<String, Object> result = new java.util.HashMap<>();
result.put("list", page.getRecords());
result.put("total", page.getTotal());
result.put("pageNum", page.getCurrent());
result.put("pageSize", page.getSize());
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 订单详情
*/
@GetMapping("/order/detail")
public Result<OrderTrade> getOrderDetail(@RequestParam String orderNo) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
OrderTrade order = tradeService.getOrderDetail(userId, orderNo);
if (order == null) {
return Result.fail("订单不存在");
}
return Result.success(order);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,120 @@
package com.it.rattan.monisuo.controller;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 用户接口
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 用户注册
*/
@PostMapping("/register")
public Result<Map<String, Object>> register(@RequestBody Map<String, String> params) {
String username = params.get("username");
String password = params.get("password");
if (username == null || username.trim().isEmpty()) {
return Result.fail("用户名不能为空");
}
if (password == null || password.length() < 6) {
return Result.fail("密码长度至少6位");
}
try {
Map<String, Object> result = userService.register(username.trim(), password);
return Result.success("注册成功", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 用户登录
*/
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody Map<String, String> params) {
String username = params.get("username");
String password = params.get("password");
if (username == null || username.trim().isEmpty()) {
return Result.fail("用户名不能为空");
}
if (password == null || password.isEmpty()) {
return Result.fail("密码不能为空");
}
try {
Map<String, Object> result = userService.login(username.trim(), password);
return Result.success("登录成功", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 获取用户信息
*/
@GetMapping("/info")
public Result<Map<String, Object>> getUserInfo() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
Map<String, Object> result = userService.getUserInfo(userId);
return Result.success(result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 上传KYC资料
*/
@PostMapping("/kyc")
public Result<Void> uploadKyc(@RequestBody Map<String, String> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
String idCardFront = params.get("idCardFront");
String idCardBack = params.get("idCardBack");
if (idCardFront == null || idCardFront.isEmpty()) {
return Result.fail("请上传身份证正面照");
}
if (idCardBack == null || idCardBack.isEmpty()) {
return Result.fail("请上传身份证反面照");
}
try {
userService.uploadKyc(userId, idCardFront, idCardBack);
return Result.success("上传成功", null);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 退出登录
*/
@PostMapping("/logout")
public Result<Void> logout() {
// 客户端清除Token即可
return Result.success("退出成功", null);
}
}

View File

@@ -0,0 +1,51 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 资金流水实体类
*/
@Data
@TableName("account_flow")
public class AccountFlow implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 用户ID */
private Long userId;
/** 流水号 */
private String flowNo;
/** 流水类型: 1-充值 2-提现 3-划转转入 4-划转转出 5-买入 6-卖出 */
private Integer flowType;
/** 变动金额 */
private BigDecimal amount;
/** 变动前余额 */
private BigDecimal balanceBefore;
/** 变动后余额 */
private BigDecimal balanceAfter;
/** 相关币种 */
private String coinCode;
/** 关联订单号 */
private String relatedOrderNo;
/** 备注 */
private String remark;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,43 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 资金账户实体类
*/
@Data
@TableName("account_fund")
public class AccountFund implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 用户ID */
private Long userId;
/** USDT余额 */
private BigDecimal balance;
/** 冻结金额 */
private BigDecimal frozen;
/** 累计充值 */
private BigDecimal totalDeposit;
/** 累计提现 */
private BigDecimal totalWithdraw;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,49 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 交易账户实体类
*/
@Data
@TableName("account_trade")
public class AccountTrade implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 用户ID */
private Long userId;
/** 币种代码 */
private String coinCode;
/** 持仓数量 */
private BigDecimal quantity;
/** 冻结数量 */
private BigDecimal frozen;
/** 平均成本价 */
private BigDecimal avgPrice;
/** 累计买入数量 */
private BigDecimal totalBuy;
/** 累计卖出数量 */
private BigDecimal totalSell;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,61 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 管理员实体类
*/
@Data
@TableName("sys_admin")
public class Admin implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 账号 */
private String username;
/** 密码(BCrypt加密) */
private String password;
/** 昵称 */
private String nickname;
/** 头像URL */
private String avatar;
/** 角色: 1-超级管理员 2-普通管理员 */
private Integer role;
/** 权限列表(JSON格式) */
private String permissions;
/** 状态: 0-禁用 1-正常 */
private Integer status;
/** 是否系统预置: 0-否 1-是 */
private Integer isSystem;
/** 最后登录时间 */
private LocalDateTime lastLoginTime;
/** 最后登录IP */
private String lastLoginIp;
/** 当前Token */
@TableField(select = false)
private String token;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,91 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 币种实体类
*/
@Data
@TableName("coin")
public class Coin implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 币种代码(如BTC) */
private String code;
/** 币种名称(如Bitcoin) */
private String name;
/** 币种图标URL */
private String icon;
/** 当前价格(USDT) */
private BigDecimal price;
/** 美元价格 */
private BigDecimal priceUsd;
/** 人民币价格 */
private BigDecimal priceCny;
/** 价格类型: 1-实时 2-管理 */
private Integer priceType;
/** 24小时涨跌幅(%) */
private BigDecimal change24h;
/** 24小时最高价 */
private BigDecimal high24h;
/** 24小时最低价 */
private BigDecimal low24h;
/** 24小时交易量 */
private BigDecimal volume24h;
/** 市值 */
private BigDecimal marketCap;
/** 总发行量 */
private BigDecimal totalSupply;
/** 流通量 */
private BigDecimal circulatingSupply;
/** 币种简介 */
private String description;
/** 官网链接 */
private String website;
/** 价格小数位 */
private Integer priceScale;
/** 数量小数位 */
private Integer quantityScale;
/** 最小交易数量 */
private BigDecimal minQuantity;
/** 状态: 0-下架 1-上架 */
private Integer status;
/** 排序权重(越大越靠前) */
private Integer sort;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,64 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 充提订单实体类
*/
@Data
@TableName("order_fund")
public class OrderFund implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 订单号 */
private String orderNo;
/** 用户ID */
private Long userId;
/** 用户账号(冗余) */
private String username;
/** 类型: 1-充值 2-提现 */
private Integer type;
/** 金额(USDT) */
private BigDecimal amount;
/** 状态: 1-待审批 2-已完成 3-已驳回 4-已取消 */
private Integer status;
/** 审批管理员ID */
private Long approveAdminId;
/** 审批管理员名称 */
private String approveAdminName;
/** 审批时间 */
private LocalDateTime approveTime;
/** 驳回原因 */
private String rejectReason;
/** 用户备注 */
private String remark;
/** 管理员备注 */
private String adminRemark;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,61 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 交易订单实体类
*/
@Data
@TableName("order_trade")
public class OrderTrade implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 订单号 */
private String orderNo;
/** 用户ID */
private Long userId;
/** 交易币种代码 */
private String coinCode;
/** 交易方向: 1-买入 2-卖出 */
private Integer direction;
/** 订单类型: 1-市价 2-限价 */
private Integer orderType;
/** 成交价格 */
private BigDecimal price;
/** 成交数量 */
private BigDecimal quantity;
/** 成交金额(USDT) */
private BigDecimal amount;
/** 手续费 */
private BigDecimal fee;
/** 状态: 1-成功 2-失败 3-已取消 */
private Integer status;
/** 备注 */
private String remark;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,68 @@
package com.it.rattan.monisuo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 用户实体类
*/
@Data
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 账号 */
private String username;
/** 密码(BCrypt加密) */
private String password;
/** 昵称 */
private String nickname;
/** 头像URL */
private String avatar;
/** 手机号 */
private String phone;
/** 邮箱 */
private String email;
/** KYC状态: 0-未激活 1-已激活 */
private Integer kycStatus;
/** 身份证正面照URL */
private String idCardFront;
/** 身份证反面照URL */
private String idCardBack;
/** 状态: 0-禁用 1-正常 */
private Integer status;
/** 最后登录时间 */
private LocalDateTime lastLoginTime;
/** 最后登录IP */
private String lastLoginIp;
/** 当前Token */
@TableField(select = false)
private String token;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,86 @@
package com.it.rattan.monisuo.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.util.JwtUtil;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Token过滤器
*/
@Component
@Order(1)
public class TokenFilter implements Filter {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/** 不需要验证的路径 */
private static final String[] EXCLUDE_PATHS = {
"/api/user/register",
"/api/user/login",
"/admin/login",
"/swagger-resources",
"/v2/api-docs",
"/webjars/",
"/swagger-ui.html"
};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String uri = httpRequest.getRequestURI();
// 检查是否排除路径
for (String excludePath : EXCLUDE_PATHS) {
if (uri.contains(excludePath)) {
chain.doFilter(request, response);
return;
}
}
// 获取Token
String token = httpRequest.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
}
if (token == null || token.isEmpty()) {
writeUnauthorized(httpResponse, "请先登录");
return;
}
// 验证Token
if (!JwtUtil.isValid(token)) {
writeUnauthorized(httpResponse, "Token已过期请重新登录");
return;
}
// 设置用户上下文
UserContext context = new UserContext();
context.setUserId(JwtUtil.getUserId(token));
context.setUsername(JwtUtil.getUsername(token));
context.setType(JwtUtil.getType(token));
UserContext.set(context);
try {
chain.doFilter(request, response);
} finally {
UserContext.clear();
}
}
private void writeUnauthorized(HttpServletResponse response, String message) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.unauthorized(message)));
}
}

View File

@@ -0,0 +1,12 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.AccountFlow;
import org.apache.ibatis.annotations.Mapper;
/**
* 资金流水Mapper
*/
@Mapper
public interface AccountFlowMapper extends BaseMapper<AccountFlow> {
}

View File

@@ -0,0 +1,26 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.AccountFund;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
/**
* 资金账户Mapper
*/
@Mapper
public interface AccountFundMapper extends BaseMapper<AccountFund> {
@Select("SELECT IFNULL(SUM(balance), 0) FROM account_fund")
BigDecimal sumAllBalance();
@Select("SELECT IFNULL(SUM(total_deposit), 0) FROM account_fund")
BigDecimal sumTotalDeposit();
@Select("SELECT IFNULL(SUM(total_withdraw), 0) FROM account_fund")
BigDecimal sumTotalWithdraw();
@Select("SELECT IFNULL(SUM(quantity * avg_price), 0) FROM account_trade")
BigDecimal sumAllTradeValue();
}

View File

@@ -0,0 +1,19 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.AccountTrade;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
/**
* 交易账户Mapper
*/
@Mapper
public interface AccountTradeMapper extends BaseMapper<AccountTrade> {
@Select("SELECT IFNULL(SUM(at.quantity * c.price), 0) FROM account_trade at " +
"LEFT JOIN coin c ON at.coin_code = c.code " +
"WHERE at.quantity > 0")
BigDecimal sumAllTradeValue();
}

View File

@@ -0,0 +1,12 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.Admin;
import org.apache.ibatis.annotations.Mapper;
/**
* 管理员Mapper
*/
@Mapper
public interface AdminMapper extends BaseMapper<Admin> {
}

View File

@@ -0,0 +1,12 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.Coin;
import org.apache.ibatis.annotations.Mapper;
/**
* 币种Mapper
*/
@Mapper
public interface CoinMapper extends BaseMapper<Coin> {
}

View File

@@ -0,0 +1,23 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.OrderFund;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
/**
* 充提订单Mapper
*/
@Mapper
public interface OrderFundMapper extends BaseMapper<OrderFund> {
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 1 AND status = 2")
BigDecimal sumCompletedDeposit();
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 2 AND status = 2")
BigDecimal sumCompletedWithdraw();
@Select("SELECT COUNT(*) FROM order_fund WHERE status = 1")
int countPending();
}

View File

@@ -0,0 +1,12 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.OrderTrade;
import org.apache.ibatis.annotations.Mapper;
/**
* 交易订单Mapper
*/
@Mapper
public interface OrderTradeMapper extends BaseMapper<OrderTrade> {
}

View File

@@ -0,0 +1,12 @@
package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户Mapper
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,256 @@
package com.it.rattan.monisuo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.it.rattan.monisuo.entity.AccountFlow;
import com.it.rattan.monisuo.entity.AccountFund;
import com.it.rattan.monisuo.entity.AccountTrade;
import com.it.rattan.monisuo.entity.Coin;
import com.it.rattan.monisuo.mapper.AccountFlowMapper;
import com.it.rattan.monisuo.mapper.AccountFundMapper;
import com.it.rattan.monisuo.mapper.AccountTradeMapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.it.rattan.monisuo.util.OrderNoUtil;
import org.springframework.beans.factory.annotation.Autowired;
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.*;
/**
* 资产服务
*/
@Service
public class AssetService {
@Autowired
private AccountFundMapper accountFundMapper;
@Autowired
private AccountTradeMapper accountTradeMapper;
@Autowired
private AccountFlowMapper accountFlowMapper;
@Autowired
private CoinService coinService;
/**
* 获取资产总览
*/
public Map<String, Object> getOverview(Long userId) {
Map<String, Object> result = new HashMap<>();
// 资金账户
AccountFund fund = getOrCreateFundAccount(userId);
result.put("fundBalance", fund.getBalance());
result.put("fundFrozen", fund.getFrozen());
// 交易账户
BigDecimal tradeValue = BigDecimal.ZERO;
List<Map<String, Object>> positions = new ArrayList<>();
LambdaQueryWrapper<AccountTrade> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountTrade::getUserId, userId)
.gt(AccountTrade::getQuantity, BigDecimal.ZERO);
List<AccountTrade> trades = accountTradeMapper.selectList(wrapper);
for (AccountTrade trade : trades) {
Coin coin = coinService.getCoinByCode(trade.getCoinCode());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice())
.setScale(8, RoundingMode.DOWN);
tradeValue = tradeValue.add(value);
Map<String, Object> position = new HashMap<>();
position.put("coinCode", trade.getCoinCode());
position.put("coinName", coin.getName());
position.put("quantity", trade.getQuantity());
position.put("price", coin.getPrice());
position.put("value", value);
position.put("avgPrice", trade.getAvgPrice());
positions.add(position);
}
}
result.put("tradeValue", tradeValue);
result.put("positions", positions);
// 总资产
BigDecimal totalAssets = fund.getBalance().add(tradeValue);
result.put("totalAssets", totalAssets);
return result;
}
/**
* 获取资金账户
*/
public AccountFund getOrCreateFundAccount(Long userId) {
LambdaQueryWrapper<AccountFund> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountFund::getUserId, userId);
AccountFund fund = accountFundMapper.selectOne(wrapper);
if (fund == null) {
fund = new AccountFund();
fund.setUserId(userId);
fund.setBalance(BigDecimal.ZERO);
fund.setFrozen(BigDecimal.ZERO);
fund.setTotalDeposit(BigDecimal.ZERO);
fund.setTotalWithdraw(BigDecimal.ZERO);
fund.setCreateTime(LocalDateTime.now());
accountFundMapper.insert(fund);
}
return fund;
}
/**
* 获取交易账户
*/
public List<Map<String, Object>> getTradeAccount(Long userId) {
List<Map<String, Object>> result = new ArrayList<>();
LambdaQueryWrapper<AccountTrade> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountTrade::getUserId, userId)
.gt(AccountTrade::getQuantity, BigDecimal.ZERO);
List<AccountTrade> trades = accountTradeMapper.selectList(wrapper);
for (AccountTrade trade : trades) {
Coin coin = coinService.getCoinByCode(trade.getCoinCode());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice())
.setScale(8, RoundingMode.DOWN);
Map<String, Object> item = new HashMap<>();
item.put("coinCode", trade.getCoinCode());
item.put("coinName", coin.getName());
item.put("coinIcon", coin.getIcon());
item.put("quantity", trade.getQuantity());
item.put("price", coin.getPrice());
item.put("value", value);
item.put("avgPrice", trade.getAvgPrice());
item.put("change24h", coin.getChange24h());
result.add(item);
}
}
return result;
}
/**
* 资金划转
*/
@Transactional
public void transfer(Long userId, Integer direction, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("划转金额必须大于0");
}
AccountFund fund = getOrCreateFundAccount(userId);
// 获取交易账户USDT持仓
AccountTrade tradeUsdt = getOrCreateTradeAccount(userId, "USDT");
if (direction == 1) {
// 资金账户 -> 交易账户
if (fund.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("资金账户余额不足");
}
fund.setBalance(fund.getBalance().subtract(amount));
tradeUsdt.setQuantity(tradeUsdt.getQuantity().add(amount));
// 记录流水
createFlow(userId, 4, amount.negate(), fund.getBalance().add(amount),
fund.getBalance(), "USDT", null, "划转至交易账户");
} else if (direction == 2) {
// 交易账户 -> 资金账户
if (tradeUsdt.getQuantity().compareTo(amount) < 0) {
throw new RuntimeException("交易账户USDT余额不足");
}
tradeUsdt.setQuantity(tradeUsdt.getQuantity().subtract(amount));
fund.setBalance(fund.getBalance().add(amount));
// 记录流水
createFlow(userId, 3, amount, fund.getBalance().subtract(amount),
fund.getBalance(), "USDT", null, "划转至资金账户");
} else {
throw new RuntimeException("无效的划转方向");
}
fund.setUpdateTime(LocalDateTime.now());
accountFundMapper.updateById(fund);
tradeUsdt.setUpdateTime(LocalDateTime.now());
accountTradeMapper.updateById(tradeUsdt);
}
/**
* 获取或创建交易账户
*/
public AccountTrade getOrCreateTradeAccount(Long userId, String coinCode) {
LambdaQueryWrapper<AccountTrade> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountTrade::getUserId, userId)
.eq(AccountTrade::getCoinCode, coinCode.toUpperCase());
AccountTrade trade = accountTradeMapper.selectOne(wrapper);
if (trade == null) {
trade = new AccountTrade();
trade.setUserId(userId);
trade.setCoinCode(coinCode.toUpperCase());
trade.setQuantity(BigDecimal.ZERO);
trade.setFrozen(BigDecimal.ZERO);
trade.setAvgPrice(BigDecimal.ZERO);
trade.setTotalBuy(BigDecimal.ZERO);
trade.setTotalSell(BigDecimal.ZERO);
trade.setCreateTime(LocalDateTime.now());
accountTradeMapper.insert(trade);
}
return trade;
}
/**
* 创建资金流水
*/
public void createFlow(Long userId, Integer flowType, BigDecimal amount,
BigDecimal balanceBefore, BigDecimal balanceAfter,
String coinCode, String relatedOrderNo, String remark) {
AccountFlow flow = new AccountFlow();
flow.setUserId(userId);
flow.setFlowNo(OrderNoUtil.flowNo());
flow.setFlowType(flowType);
flow.setAmount(amount);
flow.setBalanceBefore(balanceBefore);
flow.setBalanceAfter(balanceAfter);
flow.setCoinCode(coinCode);
flow.setRelatedOrderNo(relatedOrderNo);
flow.setRemark(remark);
flow.setCreateTime(LocalDateTime.now());
accountFlowMapper.insert(flow);
}
/**
* 获取资金流水
*/
public List<AccountFlow> getFlows(Long userId, Integer flowType, int page, int size) {
LambdaQueryWrapper<AccountFlow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountFlow::getUserId, userId);
if (flowType != null && flowType > 0) {
wrapper.eq(AccountFlow::getFlowType, flowType);
}
wrapper.orderByDesc(AccountFlow::getCreateTime);
wrapper.last("LIMIT " + (page - 1) * size + ", " + size);
return accountFlowMapper.selectList(wrapper);
}
/**
* 更新资金账户
*/
public void updateFundAccount(LambdaUpdateWrapper<AccountFund> updateWrapper) {
accountFundMapper.update(null, updateWrapper);
}
}

View File

@@ -0,0 +1,58 @@
package com.it.rattan.monisuo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.it.rattan.monisuo.entity.Coin;
import com.it.rattan.monisuo.mapper.CoinMapper;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
/**
* 币种服务
*/
@Service
public class CoinService extends ServiceImpl<CoinMapper, Coin> {
/**
* 获取所有上架币种
*/
public List<Coin> getActiveCoins() {
LambdaQueryWrapper<Coin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Coin::getStatus, 1)
.orderByDesc(Coin::getSort);
return list(wrapper);
}
/**
* 根据代码获取币种
*/
public Coin getCoinByCode(String code) {
LambdaQueryWrapper<Coin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Coin::getCode, code.toUpperCase())
.eq(Coin::getStatus, 1);
return getOne(wrapper);
}
/**
* 更新币种价格
*/
public void updatePrice(String code, BigDecimal price) {
Coin coin = getCoinByCode(code);
if (coin != null) {
coin.setPrice(price);
coin.setUpdateTime(java.time.LocalDateTime.now());
updateById(coin);
}
}
/**
* 获取实时币种列表
*/
public List<Coin> getRealTimeCoins() {
LambdaQueryWrapper<Coin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Coin::getPriceType, 1)
.eq(Coin::getStatus, 1);
return list(wrapper);
}
}

View File

@@ -0,0 +1,244 @@
package com.it.rattan.monisuo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.it.rattan.monisuo.entity.AccountFund;
import com.it.rattan.monisuo.entity.OrderFund;
import com.it.rattan.monisuo.entity.User;
import com.it.rattan.monisuo.mapper.OrderFundMapper;
import com.it.rattan.monisuo.mapper.UserMapper;
import com.it.rattan.monisuo.util.OrderNoUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
/**
* 充提服务
*/
@Service
public class FundService {
@Autowired
private OrderFundMapper orderFundMapper;
@Autowired
private AssetService assetService;
@Autowired
private UserMapper userMapper;
/**
* 申请充值
*/
@Transactional
public Map<String, Object> deposit(Long userId, BigDecimal amount, String remark) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("充值金额必须大于0");
}
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
OrderFund order = new OrderFund();
order.setOrderNo(OrderNoUtil.fundOrderNo());
order.setUserId(userId);
order.setUsername(user.getUsername());
order.setType(1); // 充值
order.setAmount(amount);
order.setStatus(1); // 待审批
order.setRemark(remark);
order.setCreateTime(LocalDateTime.now());
orderFundMapper.insert(order);
Map<String, Object> result = new HashMap<>();
result.put("orderNo", order.getOrderNo());
result.put("amount", amount);
result.put("status", order.getStatus());
return result;
}
/**
* 申请提现
*/
@Transactional
public Map<String, Object> withdraw(Long userId, BigDecimal amount, String remark) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("提现金额必须大于0");
}
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 检查余额
AccountFund fund = assetService.getOrCreateFundAccount(userId);
if (fund.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("资金账户余额不足");
}
OrderFund order = new OrderFund();
order.setOrderNo(OrderNoUtil.fundOrderNo());
order.setUserId(userId);
order.setUsername(user.getUsername());
order.setType(2); // 提现
order.setAmount(amount);
order.setStatus(1); // 待审批
order.setRemark(remark);
order.setCreateTime(LocalDateTime.now());
orderFundMapper.insert(order);
Map<String, Object> result = new HashMap<>();
result.put("orderNo", order.getOrderNo());
result.put("amount", amount);
result.put("status", order.getStatus());
return result;
}
/**
* 取消订单
*/
@Transactional
public void cancel(Long userId, String orderNo) {
LambdaQueryWrapper<OrderFund> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderFund::getUserId, userId)
.eq(OrderFund::getOrderNo, orderNo)
.eq(OrderFund::getStatus, 1); // 仅待审批可取消
OrderFund order = orderFundMapper.selectOne(wrapper);
if (order == null) {
throw new RuntimeException("订单不存在或状态不可取消");
}
order.setStatus(4); // 已取消
order.setUpdateTime(LocalDateTime.now());
orderFundMapper.updateById(order);
}
/**
* 获取充提记录
*/
public IPage<OrderFund> getOrders(Long userId, Integer type, int pageNum, int pageSize) {
LambdaQueryWrapper<OrderFund> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderFund::getUserId, userId);
if (type != null && type > 0) {
wrapper.eq(OrderFund::getType, type);
}
wrapper.orderByDesc(OrderFund::getCreateTime);
Page<OrderFund> page = new Page<>(pageNum, pageSize);
return orderFundMapper.selectPage(page, wrapper);
}
/**
* 获取待审批订单数量
*/
public int getPendingCount() {
return orderFundMapper.countPending();
}
/**
* 管理员审批
*/
@Transactional
public void approve(Long adminId, String adminName, String orderNo, Integer status,
String rejectReason, String adminRemark) {
OrderFund order = orderFundMapper.selectOne(
new LambdaQueryWrapper<OrderFund>().eq(OrderFund::getOrderNo, orderNo));
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (order.getStatus() != 1) {
throw new RuntimeException("订单已处理");
}
if (status == 2) {
// 审批通过
AccountFund fund = assetService.getOrCreateFundAccount(order.getUserId());
if (order.getType() == 1) {
// 充值:增加余额
fund.setBalance(fund.getBalance().add(order.getAmount()));
fund.setTotalDeposit(fund.getTotalDeposit().add(order.getAmount()));
} else {
// 提现:扣减余额
if (fund.getBalance().compareTo(order.getAmount()) < 0) {
throw new RuntimeException("用户余额不足");
}
fund.setBalance(fund.getBalance().subtract(order.getAmount()));
fund.setTotalWithdraw(fund.getTotalWithdraw().add(order.getAmount()));
}
fund.setUpdateTime(LocalDateTime.now());
// 更新账户
LambdaUpdateWrapper<AccountFund> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(AccountFund::getUserId, order.getUserId())
.set(AccountFund::getBalance, fund.getBalance())
.set(AccountFund::getTotalDeposit, fund.getTotalDeposit())
.set(AccountFund::getTotalWithdraw, fund.getTotalWithdraw())
.set(AccountFund::getUpdateTime, LocalDateTime.now());
assetService.updateFundAccount(updateWrapper);
// 记录流水
int flowType = order.getType() == 1 ? 1 : 2;
String remark = order.getType() == 1 ? "充值" : "提现";
assetService.createFlow(order.getUserId(), flowType, order.getAmount(),
fund.getBalance().subtract(order.getAmount()),
fund.getBalance(), "USDT", orderNo, remark);
} else if (status == 3) {
// 审批驳回
if (rejectReason == null || rejectReason.isEmpty()) {
throw new RuntimeException("请填写驳回原因");
}
order.setRejectReason(rejectReason);
}
order.setStatus(status);
order.setApproveAdminId(adminId);
order.setApproveAdminName(adminName);
order.setApproveTime(LocalDateTime.now());
order.setAdminRemark(adminRemark);
order.setUpdateTime(LocalDateTime.now());
orderFundMapper.updateById(order);
}
/**
* 获取待审批订单列表
*/
public IPage<OrderFund> getPendingOrders(int pageNum, int pageSize) {
LambdaQueryWrapper<OrderFund> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderFund::getStatus, 1)
.orderByAsc(OrderFund::getCreateTime);
Page<OrderFund> page = new Page<>(pageNum, pageSize);
return orderFundMapper.selectPage(page, wrapper);
}
/**
* 获取所有充提订单(管理员)
*/
public IPage<OrderFund> getAllOrders(Integer type, Integer status, int pageNum, int pageSize) {
LambdaQueryWrapper<OrderFund> wrapper = new LambdaQueryWrapper<>();
if (type != null && type > 0) {
wrapper.eq(OrderFund::getType, type);
}
if (status != null && status > 0) {
wrapper.eq(OrderFund::getStatus, status);
}
wrapper.orderByDesc(OrderFund::getCreateTime);
Page<OrderFund> page = new Page<>(pageNum, pageSize);
return orderFundMapper.selectPage(page, wrapper);
}
}

View File

@@ -0,0 +1,189 @@
package com.it.rattan.monisuo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.it.rattan.monisuo.entity.*;
import com.it.rattan.monisuo.mapper.OrderTradeMapper;
import com.it.rattan.monisuo.util.OrderNoUtil;
import org.springframework.beans.factory.annotation.Autowired;
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.*;
/**
* 交易服务
*/
@Service
public class TradeService {
@Autowired
private OrderTradeMapper orderTradeMapper;
@Autowired
private AssetService assetService;
@Autowired
private CoinService coinService;
/**
* 买入
*/
@Transactional
public Map<String, Object> buy(Long userId, String coinCode, BigDecimal price, BigDecimal quantity) {
Coin coin = coinService.getCoinByCode(coinCode);
if (coin == null) {
throw new RuntimeException("币种不存在");
}
if (coin.getStatus() != 1) {
throw new RuntimeException("该币种已下架");
}
// 计算金额
BigDecimal amount = price.multiply(quantity).setScale(8, RoundingMode.DOWN);
// 检查交易账户USDT余额
AccountTrade usdtAccount = assetService.getOrCreateTradeAccount(userId, "USDT");
if (usdtAccount.getQuantity().compareTo(amount) < 0) {
throw new RuntimeException("交易账户USDT余额不足请先划转资金");
}
// 扣减USDT
usdtAccount.setQuantity(usdtAccount.getQuantity().subtract(amount));
usdtAccount.setUpdateTime(LocalDateTime.now());
// 增加持仓
AccountTrade coinAccount = assetService.getOrCreateTradeAccount(userId, coinCode);
BigDecimal oldTotal = coinAccount.getQuantity().multiply(coinAccount.getAvgPrice());
BigDecimal newTotal = oldTotal.add(amount);
BigDecimal newQuantity = coinAccount.getQuantity().add(quantity);
coinAccount.setQuantity(newQuantity);
if (newQuantity.compareTo(BigDecimal.ZERO) > 0) {
coinAccount.setAvgPrice(newTotal.divide(newQuantity, 8, RoundingMode.DOWN));
}
coinAccount.setTotalBuy(coinAccount.getTotalBuy().add(quantity));
coinAccount.setUpdateTime(LocalDateTime.now());
// 创建订单
OrderTrade order = new OrderTrade();
order.setOrderNo(OrderNoUtil.tradeOrderNo());
order.setUserId(userId);
order.setCoinCode(coinCode.toUpperCase());
order.setDirection(1); // 买入
order.setOrderType(1); // 市价
order.setPrice(price);
order.setQuantity(quantity);
order.setAmount(amount);
order.setFee(BigDecimal.ZERO);
order.setStatus(1); // 成功
order.setCreateTime(LocalDateTime.now());
orderTradeMapper.insert(order);
// 记录流水
assetService.createFlow(userId, 5, amount.negate(), usdtAccount.getQuantity().add(amount),
usdtAccount.getQuantity(), "USDT", order.getOrderNo(),
"买入" + coinCode + ",数量:" + quantity);
Map<String, Object> result = new HashMap<>();
result.put("orderNo", order.getOrderNo());
result.put("price", price);
result.put("quantity", quantity);
result.put("amount", amount);
return result;
}
/**
* 卖出
*/
@Transactional
public Map<String, Object> sell(Long userId, String coinCode, BigDecimal price, BigDecimal quantity) {
Coin coin = coinService.getCoinByCode(coinCode);
if (coin == null) {
throw new RuntimeException("币种不存在");
}
if (coin.getStatus() != 1) {
throw new RuntimeException("该币种已下架");
}
// 检查持仓
AccountTrade coinAccount = assetService.getOrCreateTradeAccount(userId, coinCode);
if (coinAccount.getQuantity().compareTo(quantity) < 0) {
throw new RuntimeException("持仓数量不足");
}
// 计算金额
BigDecimal amount = price.multiply(quantity).setScale(8, RoundingMode.DOWN);
// 扣减持仓
coinAccount.setQuantity(coinAccount.getQuantity().subtract(quantity));
coinAccount.setTotalSell(coinAccount.getTotalSell().add(quantity));
coinAccount.setUpdateTime(LocalDateTime.now());
// 增加USDT
AccountTrade usdtAccount = assetService.getOrCreateTradeAccount(userId, "USDT");
usdtAccount.setQuantity(usdtAccount.getQuantity().add(amount));
usdtAccount.setUpdateTime(LocalDateTime.now());
// 创建订单
OrderTrade order = new OrderTrade();
order.setOrderNo(OrderNoUtil.tradeOrderNo());
order.setUserId(userId);
order.setCoinCode(coinCode.toUpperCase());
order.setDirection(2); // 卖出
order.setOrderType(1); // 市价
order.setPrice(price);
order.setQuantity(quantity);
order.setAmount(amount);
order.setFee(BigDecimal.ZERO);
order.setStatus(1); // 成功
order.setCreateTime(LocalDateTime.now());
orderTradeMapper.insert(order);
// 记录流水
assetService.createFlow(userId, 6, amount, usdtAccount.getQuantity().subtract(amount),
usdtAccount.getQuantity(), "USDT", order.getOrderNo(),
"卖出" + coinCode + ",数量:" + quantity);
Map<String, Object> result = new HashMap<>();
result.put("orderNo", order.getOrderNo());
result.put("price", price);
result.put("quantity", quantity);
result.put("amount", amount);
return result;
}
/**
* 获取交易记录
*/
public IPage<OrderTrade> getOrders(Long userId, String coinCode, Integer direction,
int pageNum, int pageSize) {
LambdaQueryWrapper<OrderTrade> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderTrade::getUserId, userId);
if (coinCode != null && !coinCode.isEmpty()) {
wrapper.eq(OrderTrade::getCoinCode, coinCode.toUpperCase());
}
if (direction != null && direction > 0) {
wrapper.eq(OrderTrade::getDirection, direction);
}
wrapper.orderByDesc(OrderTrade::getCreateTime);
Page<OrderTrade> page = new Page<>(pageNum, pageSize);
return orderTradeMapper.selectPage(page, wrapper);
}
/**
* 获取订单详情
*/
public OrderTrade getOrderDetail(Long userId, String orderNo) {
LambdaQueryWrapper<OrderTrade> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderTrade::getUserId, userId)
.eq(OrderTrade::getOrderNo, orderNo);
return orderTradeMapper.selectOne(wrapper);
}
}

View File

@@ -0,0 +1,156 @@
package com.it.rattan.monisuo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.entity.AccountFund;
import com.it.rattan.monisuo.entity.User;
import com.it.rattan.monisuo.mapper.AccountFundMapper;
import com.it.rattan.monisuo.mapper.UserMapper;
import com.it.rattan.monisuo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 用户服务
*/
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
@Autowired
private UserMapper userMapper;
@Autowired
private AccountFundMapper accountFundMapper;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* 用户注册
*/
@Transactional
public Map<String, Object> register(String username, String password) {
// 检查用户名是否存在
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
if (userMapper.selectCount(wrapper) > 0) {
throw new RuntimeException("用户名已存在");
}
// 创建用户
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setNickname(username);
user.setKycStatus(0);
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);
// 初始化资金账户
AccountFund fund = new AccountFund();
fund.setUserId(user.getId());
fund.setBalance(java.math.BigDecimal.ZERO);
fund.setFrozen(java.math.BigDecimal.ZERO);
fund.setTotalDeposit(java.math.BigDecimal.ZERO);
fund.setTotalWithdraw(java.math.BigDecimal.ZERO);
fund.setCreateTime(LocalDateTime.now());
accountFundMapper.insert(fund);
// 生成Token
String token = JwtUtil.createToken(user.getId(), username, "user");
// 更新Token
user.setToken(token);
userMapper.updateById(user);
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("userInfo", buildUserInfo(user));
return result;
}
/**
* 用户登录
*/
public Map<String, Object> login(String username, String password) {
// 查询用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(wrapper);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getStatus() == 0) {
throw new RuntimeException("账号已被禁用");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("密码错误");
}
// 生成新Token
String token = JwtUtil.createToken(user.getId(), username, "user");
// 更新登录信息
user.setToken(token);
user.setLastLoginTime(LocalDateTime.now());
userMapper.updateById(user);
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("userInfo", buildUserInfo(user));
return result;
}
/**
* 获取用户信息
*/
public Map<String, Object> getUserInfo(Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
return buildUserInfo(user);
}
/**
* 上传KYC资料
*/
@Transactional
public void uploadKyc(Long userId, String idCardFront, String idCardBack) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
user.setIdCardFront(idCardFront);
user.setIdCardBack(idCardBack);
user.setKycStatus(1);
user.setUpdateTime(LocalDateTime.now());
userMapper.updateById(user);
}
/**
* 构建用户信息返回
*/
private Map<String, Object> buildUserInfo(User user) {
Map<String, Object> info = new HashMap<>();
info.put("id", user.getId());
info.put("username", user.getUsername());
info.put("nickname", user.getNickname());
info.put("avatar", user.getAvatar());
info.put("phone", user.getPhone());
info.put("email", user.getEmail());
info.put("kycStatus", user.getKycStatus());
info.put("status", user.getStatus());
return info;
}
}

View File

@@ -0,0 +1,103 @@
package com.it.rattan.monisuo.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
*/
public class JwtUtil {
/** 密钥 */
private static final String SECRET = "monisuo_jwt_secret_key_2024";
/** 过期时间(7天) */
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L;
/** 签发者 */
private static final String ISSUER = "monisuo";
/**
* 生成Token
*/
public static String createToken(Long userId, String username, String type) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE_TIME);
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
return JWT.create()
.withHeader(header)
.withIssuer(ISSUER)
.withIssuedAt(now)
.withExpiresAt(expireDate)
.withClaim("userId", userId)
.withClaim("username", username)
.withClaim("type", type)
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证Token
*/
public static DecodedJWT verifyToken(String token) throws JWTVerificationException {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET))
.withIssuer(ISSUER)
.build();
return verifier.verify(token);
}
/**
* 获取用户ID
*/
public static Long getUserId(String token) {
try {
DecodedJWT jwt = verifyToken(token);
return jwt.getClaim("userId").asLong();
} catch (JWTVerificationException e) {
return null;
}
}
/**
* 获取用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = verifyToken(token);
return jwt.getClaim("username").asString();
} catch (JWTVerificationException e) {
return null;
}
}
/**
* 获取类型(user/admin)
*/
public static String getType(String token) {
try {
DecodedJWT jwt = verifyToken(token);
return jwt.getClaim("type").asString();
} catch (JWTVerificationException e) {
return null;
}
}
/**
* 检查Token是否有效
*/
public static boolean isValid(String token) {
try {
verifyToken(token);
return true;
} catch (JWTVerificationException e) {
return false;
}
}
}

View File

@@ -0,0 +1,46 @@
package com.it.rattan.monisuo.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicLong;
/**
* 订单号生成工具
*/
public class OrderNoUtil {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final AtomicLong SEQUENCE = new AtomicLong(0);
/**
* 生成订单号
* 格式: 前缀 + 时间戳 + 6位序列号
*/
public static String generate(String prefix) {
LocalDateTime now = LocalDateTime.now();
String timestamp = now.format(FORMATTER);
long seq = SEQUENCE.getAndIncrement() % 1000000;
return prefix + timestamp + String.format("%06d", seq);
}
/**
* 生成交易订单号
*/
public static String tradeOrderNo() {
return generate("T");
}
/**
* 生成充提订单号
*/
public static String fundOrderNo() {
return generate("F");
}
/**
* 生成流水号
*/
public static String flowNo() {
return generate("L");
}
}

View File

@@ -0,0 +1,41 @@
package com.it.rattan.rpc;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
public class BaseResponse {
public static final String SUCCESS = "0000";
public static final String FAIL = "0001";
private String msg;
private String code;
public BaseResponse(final String code,final String msg){
this.code = code;
this.msg = msg;
}
public static BaseResponse success(){
return new BaseResponse(BaseResponse.SUCCESS,"");
}
public static BaseResponse fail(final String message){
return new BaseResponse(BaseResponse.FAIL,message);
}
public static BaseResponse fail(final String code,final String message){
return new BaseResponse(code,message);
}
}

View File

@@ -0,0 +1,31 @@
package com.it.rattan.rpc;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
public class ObjectRestResponse<T> extends BaseResponse {
private T Data;
public ObjectRestResponse(final T data){
super(SUCCESS,"");
this.setData(data);
}
public ObjectRestResponse(final T data,final String code,final String msg){
super(code,msg);
this.setData(data);
}
public ObjectRestResponse<T> data(T data){
this.setData(data);
return this;
}
}

View File

@@ -0,0 +1,35 @@
package com.it.rattan.rpc;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.it.rattan.enums.RattanMark;
import lombok.Getter;
import lombok.Setter;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
public class RattanResponse<T> extends ObjectRestResponse<T> {
public RattanResponse(T data) {
super(data);
}
public RattanResponse(T data, String code, String msg) {
super(data, code, msg);
}
public static RattanResponse<Object> success(){
return new RattanResponse<>(RattanMark.NULL, SUCCESS,RattanMark.EMPTY);
}
public static RattanResponse<Object> success(Object data){
return new RattanResponse<>(data, SUCCESS,RattanMark.EMPTY);
}
public static RattanResponse<Object> fail(String msg){
return new RattanResponse<>(RattanMark.NULL, FAIL,msg);
}
}

View File

@@ -0,0 +1,33 @@
server:
port: 5010
spring:
datasource:
username: monisuo
password: JPJ8wYicSGC8aRnk
url: jdbc:mysql://8.155.172.147:3306/monisuo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
#mybatis-plus
mybatis-plus:
mapper-locations: classpath*:com/it/rattan/monisuo/mapper/*.xml
bean-searcher:
packages: com.it.rattan.monisuo
params:
pagination:
start: 1
ignore-case-key:
# mybatisplus代码生成器配置
generator:
username: root
password: 123admin
driver: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spccloud?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
#dbTableList: #数据库的表,可多张(自己设置)
#- rt_company
prefix:
rt

View File

@@ -0,0 +1,58 @@
server:
port: 9010
spring:
datasource:
username: root
password: 897admin$$
url: jdbc:mysql://47.97.10.240:3306/spccloud?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
initialSize: 1
minIdle: 3
maxActive: 80
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 通过connectProperties属性来打开mergeSql功能慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#mybatis-plus
mybatis-plus:
mapper-locations: classpath*:com.it.rattan.spccloud.mapper/*.xml
spccloud:
pdf-download-path: C:/Users/Administrator/Desktop/temp/
wkhtmltopdf-exe-path: C:/Users/Administrator/Desktop/wkhtmltopdf/bin/wkhtmltopdf.exe
bean-searcher:
packages: com.rattan.spccloud
params:
pagination:
start: 1
ignore-case-key:
# mybatisplus代码生成器配置
#generator:
#username: root
#password: 123admin
#driver: com.mysql.jdbc.Driver
#url: jdbc:mysql://47.97.10.240:3306/spccloud?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
#dbTableList: #数据库的表,可多张(自己设置)
#- rt_company
#prefix:
#rt

View File

@@ -0,0 +1,5 @@
spring:
profiles:
active: dev

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<!-- 只输出到控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console" />
</root>
</configuration>