1
0

Compare commits

...

4 Commits

Author SHA1 Message Date
d2b3305ca6 fix: 修复价格显示错误 2025-09-16 18:40:44 +08:00
10a0e14024 fix: 修复指标显示错位 2025-09-16 18:21:49 +08:00
a9621a10ac perf: 优化查询效率 2025-09-16 18:21:33 +08:00
b5688bd3ab feat: 查询优化 2025-09-16 18:20:59 +08:00
7 changed files with 97 additions and 78 deletions

View File

@@ -2,8 +2,12 @@ package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.Daily; import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import com.querydsl.core.types.Predicate;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -14,4 +18,12 @@ public interface DailyRepository extends SimpleRepository<Daily> {
@Query("select distinct daily.tradeDate from Daily daily where daily.stock.id = ?1") @Query("select distinct daily.tradeDate from Daily daily where daily.stock.id = ?1")
List<LocalDate> findDistinctTradeDateByStockId(Long stockId); List<LocalDate> findDistinctTradeDateByStockId(Long stockId);
@EntityGraph(attributePaths = {"stock"})
@Override
Optional<Daily> findOne(Predicate predicate);
@EntityGraph(attributePaths = {"stock"})
@Override
List<Daily> findAll(Predicate predicate, Sort sort);
} }

View File

@@ -2,8 +2,20 @@ package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator; import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import com.querydsl.core.types.Predicate;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface FinanceIndicatorRepository extends SimpleRepository<FinanceIndicator> { public interface FinanceIndicatorRepository extends SimpleRepository<FinanceIndicator> {
@EntityGraph(attributePaths = {"stock"})
@Override
Optional<FinanceIndicator> findOne(Predicate predicate);
@EntityGraph(attributePaths = {"stock"})
@Override
List<FinanceIndicator> findAll(Predicate predicate, Sort sort);
} }

View File

@@ -2,7 +2,10 @@ package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.Stock; import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import jakarta.transaction.Transactional;
import java.util.Collection;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -14,4 +17,11 @@ import org.springframework.stereotype.Repository;
public interface StockRepository extends SimpleRepository<Stock> { public interface StockRepository extends SimpleRepository<Stock> {
@Query("select distinct stock.industry from Stock stock where stock.industry is not null") @Query("select distinct stock.industry from Stock stock where stock.industry is not null")
List<String> findDistinctIndustries(); List<String> findDistinctIndustries();
@Query("select distinct stock.code from Stock stock")
List<String> findDistinctCodes();
@Modifying
@Transactional(rollbackOn = Throwable.class)
void deleteAllByCodeIn(Collection<String> code);
} }

View File

@@ -8,21 +8,20 @@ import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper; import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService; import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.annotation.LiteflowComponent; import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
@Slf4j @Slf4j
@LiteflowComponent("update_daily") @LiteflowComponent("update_daily")
public class UpdateDailyNode extends TaskNodeComponent { public class UpdateDailyNode extends NodeComponent {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final DailyRepository dailyRepository; private final DailyRepository dailyRepository;
@@ -30,8 +29,7 @@ public class UpdateDailyNode extends TaskNodeComponent {
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
public UpdateDailyNode(TaskService taskService, StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService, TransactionTemplate transactionTemplate) { public UpdateDailyNode(StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService, TransactionTemplate transactionTemplate) {
super(taskService);
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository; this.dailyRepository = dailyRepository;
this.tuShareService = tuShareService; this.tuShareService = tuShareService;
@@ -51,15 +49,11 @@ public class UpdateDailyNode extends TaskNodeComponent {
} }
var existsTradeDates = dailyRepository.findDistinctTradeDate(); var existsTradeDates = dailyRepository.findDistinctTradeDate();
var nowDate = LocalDate.now(); var nowDate = LocalDate.now();
var stocks = stockRepository.findAll(); var stocksMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock)); tradeDates.parallelStream()
var allTradeDates = tradeDates.stream()
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate)) .filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
.filter(date -> !existsTradeDates.contains(date)) .filter(date -> !existsTradeDates.contains(date))
.sorted() .filter(date -> date.isAfter(LocalDate.of(2024, 12, 31)))
.toList();
var total = new AtomicInteger(allTradeDates.size());
allTradeDates.parallelStream()
.forEach(tradeDate -> { .forEach(tradeDate -> {
var factorResponse = tuShareService.factorList(tradeDate); var factorResponse = tuShareService.factorList(tradeDate);
var factorMap = new HashMap<String, Double>(); var factorMap = new HashMap<String, Double>();

View File

@@ -126,6 +126,13 @@ public class UpdateFinanceIndicatorNode extends TaskNodeComponent {
(existing, replacement) -> existing (existing, replacement) -> existing
)); ));
var financeIndicatorsMap = financeIndicatorRepository.findAll(QFinanceIndicator.financeIndicator.year.eq(year))
.stream()
.collect(Collectors.toMap(
indicator -> indicator.getStock().getCode(),
indicator -> indicator
));
for (Stock stock : stocks) { for (Stock stock : stocks) {
var balance = balancesMap.get(stock.getCode()); var balance = balancesMap.get(stock.getCode());
var income = incomesMap.get(stock.getCode()); var income = incomesMap.get(stock.getCode());
@@ -134,10 +141,7 @@ public class UpdateFinanceIndicatorNode extends TaskNodeComponent {
if (ArrayUtil.<Object>isAllNull(balance, income, cashFlow, finaIndicator)) { if (ArrayUtil.<Object>isAllNull(balance, income, cashFlow, finaIndicator)) {
continue; continue;
} }
var indicator = financeIndicatorRepository.findOne( var indicator = financeIndicatorsMap.getOrDefault(stock.getCode(), new FinanceIndicator());
QFinanceIndicator.financeIndicator.stock.id.eq(stock.getId())
.and(QFinanceIndicator.financeIndicator.year.eq(year))
).orElse(new FinanceIndicator());
indicator.setStock(stock); indicator.setStock(stock);
indicator.setYear(year); indicator.setYear(year);
if (ObjectUtil.isNotNull(balance)) { if (ObjectUtil.isNotNull(balance)) {

View File

@@ -8,7 +8,6 @@ import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent; import com.yomahub.liteflow.core.NodeComponent;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.HashSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@LiteflowComponent("update_stock") @LiteflowComponent("update_stock")
@@ -25,44 +24,32 @@ public class UpdateStockNode extends NodeComponent {
@Transactional(rollbackOn = Throwable.class) @Transactional(rollbackOn = Throwable.class)
@Override @Override
public void process() { public void process() {
var stocks = stockRepository.findAll(); var existsStockMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock)); var stocks = tuShareService.stockList()
var targetCodes = new HashSet<String>();
tuShareService.stockList()
.data() .data()
.items() .items()
.forEach(item -> { .stream()
.map(item -> {
var code = item.get(0); var code = item.get(0);
var name = item.get(1); var name = item.get(1);
var fullname = item.get(2); var fullname = item.get(2);
var market = EnumUtil.fromString(Stock.Market.class, item.get(3)); var market = EnumUtil.fromString(Stock.Market.class, item.get(3));
var industry = item.get(4); var industry = item.get(4);
var listedDate = LocalDate.parse(item.get(5), TuShareService.TRADE_FORMAT); var listedDate = LocalDate.parse(item.get(5), TuShareService.TRADE_FORMAT);
if (stocksMap.containsKey(code)) { var stock = existsStockMap.getOrDefault(code, new Stock());
var stock = stocksMap.get(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListedDate(listedDate);
} else {
var stock = new Stock();
stock.setCode(code); stock.setCode(code);
stock.setName(name); stock.setName(name);
stock.setFullname(fullname); stock.setFullname(fullname);
stock.setMarket(market); stock.setMarket(market);
stock.setIndustry(industry); stock.setIndustry(industry);
stock.setListedDate(listedDate); stock.setListedDate(listedDate);
stocks.add(stock); return stock;
} })
targetCodes.add(code);
});
var deleteStocks = stocks.stream()
.filter(stock -> !targetCodes.contains(stock.getCode()))
.map(Stock::getId)
.toList(); .toList();
stockRepository.deleteByIds(deleteStocks); var currentCodes = stocks.stream().map(Stock::getCode).toList();
var existsCodes = stockRepository.findDistinctCodes();
var deleteCodes = existsCodes.stream().filter(code -> !currentCodes.contains(code)).toList();
stockRepository.deleteAllByCodeIn(deleteCodes);
stockRepository.saveAll(stocks); stockRepository.saveAll(stocks);
} }
} }

View File

@@ -3,6 +3,7 @@ import {useParams} from 'react-router'
import {amisRender, commonInfo, readOnlyDialogOptions, remoteMappings} from '../../util/amis.tsx' import {amisRender, commonInfo, readOnlyDialogOptions, remoteMappings} from '../../util/amis.tsx'
import type {Schema} from 'amis' import type {Schema} from 'amis'
import {isNil} from 'es-toolkit' import {isNil} from 'es-toolkit'
import {toNumber} from 'es-toolkit/compat'
const formatFinanceNumber = (value: number): string => { const formatFinanceNumber = (value: number): string => {
if (isNil(value)) { if (isNil(value)) {
@@ -280,8 +281,8 @@ function StockDetail() {
content: '${balanceSheet.currentAssets}', content: '${balanceSheet.currentAssets}',
}, },
{ {
label: financePropertyLabel(id, '流动资产占比', 'PERCENTAGE', 'fixedAssetsToTotalAssetsRatio'), label: financePropertyLabel(id, '流动资产占比', 'PERCENTAGE', 'currentAssetsToTotalAssetsRatio'),
content: '${balanceSheet.fixedAssetsRatio}', content: '${balanceSheet.currentAssetsRatio}',
}, },
{ {
label: financePropertyLabel(id, '流动负债', 'FINANCE', 'currentLiabilities'), label: financePropertyLabel(id, '流动负债', 'FINANCE', 'currentLiabilities'),
@@ -291,14 +292,14 @@ function StockDetail() {
label: financePropertyLabel(id, '流动负债占比', 'PERCENTAGE', 'currentLiabilitiesToTotalAssetsRatio'), label: financePropertyLabel(id, '流动负债占比', 'PERCENTAGE', 'currentLiabilitiesToTotalAssetsRatio'),
content: '${balanceSheet.currentLiabilitiesRatio}', content: '${balanceSheet.currentLiabilitiesRatio}',
}, },
{
label: financePropertyLabel(id, '流动资产占比', 'PERCENTAGE', 'currentAssetsToTotalAssetsRatio'),
content: '${balanceSheet.currentAssetsRatio}',
},
{ {
label: financePropertyLabel(id, '非流动资产', 'FINANCE', 'fixedAssets'), label: financePropertyLabel(id, '非流动资产', 'FINANCE', 'fixedAssets'),
content: '${balanceSheet.fixedAssets}', content: '${balanceSheet.fixedAssets}',
}, },
{
label: financePropertyLabel(id, '非流动资产占比', 'PERCENTAGE', 'fixedAssetsToTotalAssetsRatio'),
content: '${balanceSheet.fixedAssetsRatio}',
},
{ {
label: financePropertyLabel(id, '非流动负债', 'FINANCE', 'longTermLiabilities'), label: financePropertyLabel(id, '非流动负债', 'FINANCE', 'longTermLiabilities'),
content: '${balanceSheet.longTermLiabilities}', content: '${balanceSheet.longTermLiabilities}',
@@ -409,10 +410,10 @@ function StockDetail() {
], ],
}, },
{type: 'divider'}, {type: 'divider'},
"100日线数据", "100日线数据",
{ {
type: 'chart', type: 'chart',
height: 800, height: 500,
api: `get:${commonInfo.baseUrl}/stock/daily/${id}`, api: `get:${commonInfo.baseUrl}/stock/daily/${id}`,
config: { config: {
backgroundColor: '#fff', backgroundColor: '#fff',
@@ -433,30 +434,28 @@ function StockDetail() {
padding: 12, padding: 12,
formatter: function (params: any) { formatter: function (params: any) {
const param = params[0] const param = params[0]
const open = param.data[0] const open = toNumber(param.data[1]).toFixed(2)
const close = param.data[1] const close = toNumber(param.data[2]).toFixed(2)
const lowest = param.data[2] const lowest = toNumber(param.data[3]).toFixed(2)
const highest = param.data[3] const highest = toNumber(param.data[4]).toFixed(2)
return [ return `<div class="text-center font-bold mb-2">${param.name}</div>
`<div style="font-weight: bold; margin-bottom: 4px;">${param.name}</div>`, <div class="text-center">
`<div style="display: flex; justify-content: space-between; margin: 2px 0;">`, <span>开盘:</span>
`<span>开盘:</span>`, <span class="font-bold ml-4">${open}</span>
`<span style="margin-left: 12px; font-weight: bold;">${open}</span>`, </div>
`</div>`, <div class="text-center">
`<div style="display: flex; justify-content: space-between; margin: 2px 0;">`, <span>收盘:</span>
`<span>收盘:</span>`, <span class="font-bold ml-4">${close}</span>
`<span style="margin-left: 12px; font-weight: bold;">${close}</span>`, </div>
`</div>`, <div class="text-center">
`<div style="display: flex; justify-content: space-between; margin: 2px 0;">`, <span>最低:</span>
`<span>最低:</span>`, <span class="font-bold ml-4">${lowest}</span>
`<span style="margin-left: 12px; font-weight: bold;">${lowest}</span>`, </div>
`</div>`, <div class="text-center">
`<div style="display: flex; justify-content: space-between; margin: 2px 0;">`, <span>最高:</span>
`<span>最高:</span>`, <span class="font-bold ml-4">${highest}</span>
`<span style="margin-left: 12px; font-weight: bold;">${highest}</span>`, </div>`
`</div>`,
].join('')
}, },
}, },
grid: { grid: {
@@ -495,7 +494,7 @@ function StockDetail() {
color: '#666', color: '#666',
fontWeight: 'bold', fontWeight: 'bold',
formatter: function (value: number) { formatter: function (value: number) {
return value.toFixed(0) return value.toFixed(2)
}, },
}, },
splitLine: { splitLine: {
@@ -538,6 +537,7 @@ function StockDetail() {
], ],
}, },
}, },
"12月线数据",
], ],
}, },
], ],