1
0

feat(all): 初始化版本

This commit is contained in:
2025-08-30 10:10:11 +08:00
commit 7f3b61b854
55 changed files with 11289 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package com.lanyuanxiaoyao.leopard.server;
/**
* 静态字段
*
* @author lanyuanxiaoyao
* @version 20250829
*/
public interface Constants {
String DATABASE_PREFIX = "leopard_";
}

View File

@@ -0,0 +1,28 @@
package com.lanyuanxiaoyao.leopard.server;
import com.blinkfox.fenix.EnableFenix;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
/**
* @author lanyuanxiaoyao
* @version 20250828
*/
@Slf4j
@SpringBootApplication
@EnableFenix
@EnableJpaAuditing
public class LeopardServerApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(LeopardServerApplication.class, args);
}
@Override
public void run(ApplicationArguments args) {
}
}

View File

@@ -0,0 +1,29 @@
package com.lanyuanxiaoyao.leopard.server.configuration;
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;
/**
* 网络配置
*
* @author lanyuanxiaoyao
* @version 20250826
*/
@Configuration
public class WebConfiguration {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setMaxAge(7200L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,142 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 下拉菜单枚举类型
*
* @author lanyuanxiaoyao
* @version 20250826
*/
@Slf4j
@RestController
@RequestMapping("constants")
public class CommonOptionsController {
private static final List<String> COLORS = List.of(
"bg-black",
"bg-primary",
"bg-secondary",
"bg-success",
"bg-info",
"bg-warning",
"bg-danger",
"bg-dark",
"bg-gray-500",
"bg-gray-600",
"bg-gray-700",
"bg-gray-800",
"bg-gray-900",
"bg-red-500",
"bg-red-600",
"bg-red-700",
"bg-red-800",
"bg-red-900",
"bg-yellow-500",
"bg-yellow-600",
"bg-yellow-700",
"bg-yellow-800",
"bg-yellow-900",
"bg-green-500",
"bg-green-600",
"bg-green-700",
"bg-green-800",
"bg-green-900",
"bg-blue-500",
"bg-blue-600",
"bg-blue-700",
"bg-blue-800",
"bg-blue-900",
"bg-cyan-500",
"bg-cyan-600",
"bg-cyan-700",
"bg-cyan-800",
"bg-cyan-900",
"bg-indigo-500",
"bg-indigo-600",
"bg-indigo-700",
"bg-indigo-800",
"bg-indigo-900",
"bg-purple-500",
"bg-purple-600",
"bg-purple-700",
"bg-purple-800",
"bg-purple-900",
"bg-pink-500",
"bg-pink-600",
"bg-pink-700",
"bg-pink-800",
"bg-pink-900"
);
private final StockService stockService;
public CommonOptionsController(StockService stockService) {
this.stockService = stockService;
}
@GetMapping("/options/{name}")
public GlobalResponse<List<Option>> options(@PathVariable("name") String name) {
return switch (name) {
case "stock_market" -> GlobalResponse.responseSuccess(
Arrays.stream(Stock.Market.values())
.map(market -> new Option(market.getChineseName(), market.name()))
.toList()
);
case "stock_industry" -> GlobalResponse.responseSuccess(
stockService.findDistinctIndustries()
.stream()
.map(industry -> new Option(industry, industry))
.toList()
);
default -> GlobalResponse.responseSuccess(List.of());
};
}
@GetMapping("/mappings/{name}/{field}")
public GlobalResponse<Map<String, String>> mappings(@PathVariable("name") String name, @PathVariable("field") String field) {
return switch (name) {
case "stock_market" -> GlobalResponse.responseSuccess(buildMapping(
Arrays.stream(Stock.Market.values())
.map(market -> new Mapping(market.name(), market.getChineseName()))
.toList(),
field
));
case "task_status" -> GlobalResponse.responseSuccess(buildMapping(
Arrays.stream(Task.Status.values())
.map(status -> new Mapping(status.name(), status.getChineseName()))
.toList(),
field
));
default -> GlobalResponse.responseSuccess(Map.of());
};
}
private Map<String, String> buildMapping(List<Mapping> mappings, String field) {
var map = mappings
.stream()
.collect(Collectors.toMap(Mapping::name, mapping -> {
var color = COLORS.get(Math.abs(mapping.name.hashCode()) % COLORS.size());
return "<span class='label %s'>%s</span>".formatted(color, mapping.label());
}));
map.put("*", "<span class='label bg-gray-300'>%s</span>".formatted(field));
return map;
}
public record Option(String label, Object value) {
}
public record Mapping(String name, String label) {
}
}

View File

@@ -0,0 +1,62 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.util.function.Function;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lanyuanxiaoyao
* @version 20250829
*/
@RestController
@RequestMapping("stock")
public class StockController extends SimpleControllerSupport<Stock, Void, StockController.DetailItem, StockController.DetailItem> {
public StockController(StockService service) {
super(service);
}
@Override
public GlobalResponse<Long> save(Void unused) {
throw new UnsupportedOperationException();
}
@Override
protected Function<Void, Stock> saveItemMapper() {
throw new UnsupportedOperationException();
}
private DetailItem covert(Stock stock) {
return new DetailItem(
stock.getId(),
stock.getCode(),
stock.getName(),
stock.getFullname(),
stock.getMarket(),
stock.getIndustry()
);
}
@Override
protected Function<Stock, DetailItem> listItemMapper() {
return this::covert;
}
@Override
protected Function<Stock, DetailItem> detailItemMapper() {
return this::covert;
}
public record DetailItem(
Long id,
String code,
String name,
String fullname,
Stock.Market market,
String industry
) {
}
}

View File

@@ -0,0 +1,84 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.time.LocalDateTime;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 任务列表
*
* @author lanyuanxiaoyao
* @version 20250829
*/
@Slf4j
@RestController
@RequestMapping("task")
public class TaskController extends SimpleControllerSupport<Task, Void, TaskController.ListItem, TaskController.DetailItem> {
public TaskController(TaskService service) {
super(service);
}
@Override
public GlobalResponse<Long> save(Void unused) {
throw new UnsupportedOperationException();
}
@Override
protected Function<Void, Task> saveItemMapper() {
throw new UnsupportedOperationException();
}
@Override
protected Function<Task, ListItem> listItemMapper() {
return task -> new ListItem(
task.getId(),
task.getName(),
task.getDescription(),
task.getStatus(),
task.getLaunchedTime(),
task.getFinishedTime()
);
}
@Override
protected Function<Task, DetailItem> detailItemMapper() {
return task -> new DetailItem(
task.getId(),
task.getName(),
task.getDescription(),
task.getStatus(),
task.getError(),
task.getResult(),
task.getLaunchedTime(),
task.getFinishedTime()
);
}
public record ListItem(
Long id,
String name,
String description,
Task.Status status,
LocalDateTime launchedTime,
LocalDateTime finishedTime
) {
}
public record DetailItem(
Long id,
String name,
String description,
Task.Status status,
String error,
String result,
LocalDateTime launchedTime,
LocalDateTime finishedTime
) {
}
}

View File

@@ -0,0 +1,69 @@
package com.lanyuanxiaoyao.leopard.server.entity;
import com.lanyuanxiaoyao.leopard.server.Constants;
import com.lanyuanxiaoyao.leopard.server.entity.base.SimpleEnum;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* 股票
*
* @author lanyuanxiaoyao
* @version 20250828
*/
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@SoftDelete
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "stock")
public class Stock extends SimpleEntity {
@Column(unique = true, nullable = false)
@Comment("股票代码")
private String code;
@Column(nullable = false)
@Comment("股票名称")
private String name;
@Column(nullable = false)
@Comment("股票全称")
private String fullname;
@Column(nullable = false)
@Comment("交易市场")
@Enumerated(EnumType.STRING)
private Market market;
@Column(nullable = false)
@Comment("行业")
private String industry;
@Column(nullable = false)
@Comment("上市状态")
private boolean listed = true;
@Getter
@AllArgsConstructor
public enum Market implements SimpleEnum {
SSE("上交所"),
SZSE("深交所"),
BSE("北交所");
private final String chineseName;
}
}

View File

@@ -0,0 +1,80 @@
package com.lanyuanxiaoyao.leopard.server.entity;
import com.lanyuanxiaoyao.leopard.server.Constants;
import com.lanyuanxiaoyao.leopard.server.entity.base.SimpleEnum;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* 任务
*
* @author lanyuanxiaoyao
* @version 20250829
*/
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@SoftDelete
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "task")
public class Task extends SimpleEntity {
@Column(nullable = false)
@Comment("任务名称")
private String name;
@Lob
@Comment("任务描述")
private String description;
@Lob
@Basic(fetch = FetchType.LAZY)
@ToString.Exclude
@Comment("错误信息")
private String error;
@Lob
@Basic(fetch = FetchType.LAZY)
@ToString.Exclude
@Comment("任务结果")
private String result;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
@Comment("任务状态")
private Status status;
@Comment("任务开始时间")
private LocalDateTime launchedTime;
@Comment("任务结束时间")
private LocalDateTime finishedTime;
@Getter
@AllArgsConstructor
public enum Status implements SimpleEnum {
PENDING("待执行"),
RUNNING("执行中"),
SUCCESS("成功"),
FAILURE("失败"),
CANCELED("取消");
private final String chineseName;
}
}

View File

@@ -0,0 +1,9 @@
package com.lanyuanxiaoyao.leopard.server.entity.base;
/**
* @author lanyuanxiaoyao
* @version 20250829
*/
public interface SimpleEnum {
String getChineseName();
}

View File

@@ -0,0 +1,17 @@
package com.lanyuanxiaoyao.leopard.server.repository;
import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
/**
* @author lanyuanxiaoyao
* @version 20250828
*/
@Repository
public interface StockRepository extends SimpleRepository<Stock> {
@Query("select distinct stock.industry from Stock stock where stock.industry is not null")
List<String> findDistinctIndustries();
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.leopard.server.repository;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
/**
* @author lanyuanxiaoyao
* @version 20250829
*/
@Repository
public interface TaskRepository extends SimpleRepository<Task> {
}

View File

@@ -0,0 +1,70 @@
package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.repository.StockRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lanyuanxiaoyao
* @version 20250828
*/
@Slf4j
@Service
public class StockService extends SimpleServiceSupport<Stock> {
private final StockRepository stockRepository;
private final TuShareService tuShareService;
public StockService(StockRepository repository, TuShareService tuShareService) {
super(repository);
this.stockRepository = repository;
this.tuShareService = tuShareService;
}
public List<String> findDistinctIndustries() {
return stockRepository.findDistinctIndustries();
}
@Transactional(rollbackFor = Throwable.class)
public void refresh() {
log.info("开始刷新股票列表");
var stocks = list();
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
tuShareService.stockList()
.data()
.items()
.forEach(item -> {
var code = item.get(0);
var name = item.get(1);
var fullname = item.get(2);
var market = EnumUtil.fromString(Stock.Market.class, item.get(3));
var industry = item.get(4);
var listed = StrUtil.equals("L", item.get(5));
if (stocksMap.containsKey(code)) {
var stock = stocksMap.get(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListed(listed);
} else {
var stock = new Stock();
stock.setCode(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListed(listed);
stocks.add(stock);
}
});
repository.saveAll(stocks);
log.info("股票列表刷新完成");
}
}

View File

@@ -0,0 +1,22 @@
package com.lanyuanxiaoyao.leopard.server.service;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.repository.TaskRepository;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author lanyuanxiaoyao
* @version 20250829
*/
@Slf4j
@Service
public class TaskService extends SimpleServiceSupport<Task> {
public TaskService(TaskRepository repository) {
super(repository);
}
}

View File

@@ -0,0 +1,67 @@
package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.http.HttpUtil;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.stereotype.Service;
/**
* 对接TuShare接口
*
* @author lanyuanxiaoyao
* @version 20250828
*/
@Slf4j
@Service
public class TuShareService {
private static final String API_URL = "https://api.tushare.pro";
private static final String API_TOKEN = "64ebff4fa679167600b905ee45dd88e76f3963c0ff39157f3f085f0e";
private final ObjectMapper mapper;
public TuShareService(Jackson2ObjectMapperBuilder builder) {
this.mapper = builder.build();
}
@SneakyThrows
private String buildRequest(String apiName, Map<String, Object> params, List<String> fields) {
return mapper.writeValueAsString(Map.of(
"api_name", apiName,
"token", API_TOKEN,
"params", params,
"fields", fields
));
}
@SneakyThrows
public TuShareResponse stockList() {
var response = HttpUtil.post(API_URL, buildRequest(
"stock_basic",
Map.of("list_status", "L"),
List.of("ts_code", "name", "fullname", "exchange", "industry", "list_status")
));
var tuShareResponse = mapper.readValue(response, TuShareResponse.class);
if (tuShareResponse.code != 0) {
throw new RuntimeException(tuShareResponse.message);
}
return tuShareResponse;
}
public record TuShareResponse(
Integer code,
@JsonProperty("msg")
String message,
Data data
) {
public record Data(
List<String> fields,
List<List<String>> items
) {
}
}
}

View File

@@ -0,0 +1,20 @@
package com.lanyuanxiaoyao.leopard.server.task;
import java.util.Map;
/**
* 任务
*
* @author lanyuanxiaoyao
* @version 20250829
*/
public abstract class TaskTemplate {
private String name;
private String description;
public abstract Type getType();
public enum Type {
CLASS
}
}

View File

@@ -0,0 +1,17 @@
server:
port: 9786
spring:
application:
name: leopard-server
mvc:
async:
request-timeout: 3600000
datasource:
url: jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/leopard?useSSL=false
username: leopard
password: '9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X'
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
generate-ddl: true
fenix:
print-banner: false

View File

@@ -0,0 +1,24 @@
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<springProperty scope="context" name="LOGGING_PARENT" source="logging.parent"/>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%5p) %clr([${HOSTNAME}]){yellow} %clr([%t]){magenta} %clr(%logger{40}){cyan}: %m%n%wEx</pattern>
</encoder>
</appender>
<logger name="com.zaxxer.hikari" level="ERROR"/>
<logger name="org.hibernate.SQL" level="DEBUG"/>
<logger name="org.hibernate.type.descriptor.jdbc.BasicBinder" level="TRACE"/>
<root level="INFO">
<appender-ref ref="Console"/>
</root>
</configuration>