feat: 增加股票集展示
This commit is contained in:
74
.idea/compiler.xml
generated
74
.idea/compiler.xml
generated
@@ -85,78 +85,6 @@
|
|||||||
<entry name="$MAVEN_REPOSITORY$/org/easymock/easymock/5.6.0/easymock-5.6.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/easymock/easymock/5.6.0/easymock-5.6.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.4/objenesis-3.4.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.4/objenesis-3.4.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-configuration-processor/3.5.0/spring-boot-configuration-processor-3.5.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-jpamodelgen/6.6.8.Final/hibernate-jpamodelgen-6.6.8.Final.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-core/6.6.8.Final/hibernate-core-6.6.8.Final.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jboss/logging/jboss-logging/3.5.0.Final/jboss-logging-3.5.0.Final.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/common/hibernate-commons-annotations/7.0.3.Final/hibernate-commons-annotations-7.0.3.Final.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/jandex/3.2.0/jandex-3.2.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.15.11/byte-buddy-1.15.11.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/xml/bind/jakarta.xml.bind-api/4.0.0/jakarta.xml.bind-api-4.0.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/activation/jakarta.activation-api/2.1.0/jakarta.activation-api-2.1.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/jaxb-runtime/4.0.2/jaxb-runtime-4.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/jaxb-core/4.0.2/jaxb-core-4.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/eclipse/angus/angus-activation/2.0.0/angus-activation-2.0.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/txw2/4.0.2/txw2-4.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/sun/istack/istack-commons-runtime/4.1.1/istack-commons-runtime-4.1.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/validation/jakarta.validation-api/3.0.2/jakarta.validation-api-3.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-apt/7.0/querydsl-apt-7.0-jpa.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen/7.0/querydsl-codegen-7.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-core/7.0/querydsl-core-7.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/projectreactor/reactor-core/3.7.6/reactor-core-3.7.6.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen-utils/7.0/querydsl-codegen-utils-7.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/eclipse/jdt/ecj/3.40.0/ecj-3.40.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/classgraph/classgraph/4.8.179/classgraph-4.8.179.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/26.0.2/annotations-26.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/dev/morphia/morphia/morphia-core/2.5.0/morphia-core-2.5.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config/3.10.1/smallrye-config-3.10.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config-core/3.10.1/smallrye-config-core-3.10.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/eclipse/microprofile/config/microprofile-config-api/3.1/microprofile-config-api-3.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-annotation/2.8.0/smallrye-common-annotation-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-expression/2.8.0/smallrye-common-expression-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-function/2.8.0/smallrye-common-function-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-constraint/2.8.0/smallrye-common-constraint-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-classloader/2.8.0/smallrye-common-classloader-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/ow2/asm/asm/9.8/asm-9.8.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config-common/3.10.1/smallrye-config-common-3.10.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-sync/5.4.0/mongodb-driver-sync-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/bson/5.4.0/bson-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-core/5.4.0/mongodb-driver-core-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/bson-record-codec/5.4.0/bson-record-codec-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-legacy/5.4.0/mongodb-driver-legacy-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/github/spotbugs/spotbugs-annotations/4.8.6/spotbugs-annotations-4.8.6.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/semver4j/semver4j/5.6.0/semver4j-5.6.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jsoup/jsoup/1.18.3/jsoup-1.18.3.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-envers/7.0.0.Beta1/hibernate-envers-7.0.0.Beta1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/models/hibernate-models/0.8.6/hibernate-models-0.8.6.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-core/7.0/querydsl-core-7.0-tests.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/joda/joda-money/2.0.2/joda-money-2.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter/5.13.1/junit-jupiter-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.13.1/junit-jupiter-api-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.13.1/junit-platform-commons-1.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-params/5.13.1/junit-jupiter-params-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-engine/5.13.1/junit-jupiter-engine-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-engine/1.13.1/junit-platform-engine-1.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/assertj/assertj-core/3.27.3/assertj-core-3.27.3.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/vintage/junit-vintage-engine/5.13.1/junit-vintage-engine-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/junit/junit/4.13.2/junit-4.13.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/easymock/easymock/5.6.0/easymock-5.6.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.4/objenesis-3.4.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar" />
|
|
||||||
</processorPath>
|
</processorPath>
|
||||||
<module name="leopard-core" />
|
<module name="leopard-core" />
|
||||||
</profile>
|
</profile>
|
||||||
@@ -167,8 +95,6 @@
|
|||||||
<processorPath useClasspath="false">
|
<processorPath useClasspath="false">
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-configuration-processor/3.5.0/spring-boot-configuration-processor-3.5.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-configuration-processor/3.5.0/spring-boot-configuration-processor-3.5.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-configuration-processor/3.5.0/spring-boot-configuration-processor-3.5.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
|
|
||||||
</processorPath>
|
</processorPath>
|
||||||
<module name="leopard-strategy" />
|
<module name="leopard-strategy" />
|
||||||
<module name="leopard-server" />
|
<module name="leopard-server" />
|
||||||
|
|||||||
@@ -59,6 +59,15 @@
|
|||||||
<artifactId>hutool-http</artifactId>
|
<artifactId>hutool-http</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
||||||
|
<artifactId>quantlib</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ta4j</groupId>
|
||||||
|
<artifactId>ta4j-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.mysql</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>mysql-connector-j</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.controller;
|
package com.lanyuanxiaoyao.leopard.server.controller;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
||||||
|
import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.StockCollectionService;
|
import com.lanyuanxiaoyao.leopard.server.service.StockCollectionService;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.StockService;
|
import com.lanyuanxiaoyao.leopard.server.service.StockService;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
collection.getDescription(),
|
collection.getDescription(),
|
||||||
collection.getStocks().size(),
|
collection.getStocks().size(),
|
||||||
collection.getStocks()
|
collection.getStocks()
|
||||||
|
.stream()
|
||||||
|
.map(StockDetailVo::of)
|
||||||
|
.collect(Collectors.toSet())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
String name,
|
String name,
|
||||||
String description,
|
String description,
|
||||||
Integer count,
|
Integer count,
|
||||||
Set<Stock> stocks
|
Set<StockDetailVo> stocks
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.lanyuanxiaoyao.leopard.server.controller;
|
|||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo;
|
||||||
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
|
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.StockService;
|
import com.lanyuanxiaoyao.leopard.server.service.StockService;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
@@ -24,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("stock")
|
@RequestMapping("stock")
|
||||||
public class StockController extends SimpleControllerSupport<Stock, Void, StockController.DetailItem, StockController.DetailItem> {
|
public class StockController extends SimpleControllerSupport<Stock, Void, StockDetailVo, StockDetailVo> {
|
||||||
private final StockService stockService;
|
private final StockService stockService;
|
||||||
|
|
||||||
public StockController(StockService service, StockService stockService) {
|
public StockController(StockService service, StockService stockService) {
|
||||||
@@ -126,37 +127,14 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockC
|
|||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DetailItem covert(Stock stock) {
|
@Override
|
||||||
return new DetailItem(
|
protected Function<Stock, StockDetailVo> listItemMapper() {
|
||||||
stock.getId(),
|
return StockDetailVo::of;
|
||||||
stock.getCode(),
|
|
||||||
stock.getName(),
|
|
||||||
stock.getFullname(),
|
|
||||||
stock.getMarket(),
|
|
||||||
stock.getIndustry(),
|
|
||||||
stock.getListedDate()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Function<Stock, DetailItem> listItemMapper() {
|
protected Function<Stock, StockDetailVo> detailItemMapper() {
|
||||||
return this::covert;
|
return StockDetailVo::of;
|
||||||
}
|
|
||||||
|
|
||||||
@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,
|
|
||||||
LocalDate listedDate
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record FinanceItem(
|
public record FinanceItem(
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.server.entity;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250917
|
||||||
|
*/
|
||||||
|
public record StockDetailVo(
|
||||||
|
Long id,
|
||||||
|
String code,
|
||||||
|
String name,
|
||||||
|
String fullname,
|
||||||
|
Stock.Market market,
|
||||||
|
String industry,
|
||||||
|
LocalDate listedDate
|
||||||
|
) {
|
||||||
|
public static StockDetailVo of(Stock stock) {
|
||||||
|
return new StockDetailVo(
|
||||||
|
stock.getId(),
|
||||||
|
stock.getCode(),
|
||||||
|
stock.getName(),
|
||||||
|
stock.getFullname(),
|
||||||
|
stock.getMarket(),
|
||||||
|
stock.getIndustry(),
|
||||||
|
stock.getListedDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.server.service.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
||||||
|
import com.yomahub.liteflow.core.NodeComponent;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金字塔选股
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250917
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@LiteflowComponent("pyramid_stock_selector")
|
||||||
|
public class PyramidStockSelector extends NodeComponent {
|
||||||
|
private final StockRepository stockRepository;
|
||||||
|
private final StockCollectionRepository stockCollectionRepository;
|
||||||
|
|
||||||
|
public PyramidStockSelector(StockRepository stockRepository, StockCollectionRepository stockCollectionRepository) {
|
||||||
|
this.stockRepository = stockRepository;
|
||||||
|
this.stockCollectionRepository = stockCollectionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process() {
|
||||||
|
// 选择至少有最近5年财报的股票
|
||||||
|
var stocks = stockRepository.findAllByIndicatorsSizeGreaterThanEqual(5);
|
||||||
|
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
|
||||||
|
var scores = stocks.stream().map(Stock::getCode).collect(Collectors.toMap(code -> code, code -> 0));
|
||||||
|
for (Stock stock : stocks) {
|
||||||
|
var recentIndicators = stock.getIndicators()
|
||||||
|
.stream()
|
||||||
|
.sorted((a, b) -> b.getYear() - a.getYear())
|
||||||
|
.limit(5)
|
||||||
|
.toList();
|
||||||
|
var latestIndicator = recentIndicators.getFirst();
|
||||||
|
|
||||||
|
var roeScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnEquity() == null || indicator.getReturnOnEquity() < 0)) {
|
||||||
|
var averageRoe = recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getReturnOnEquity)
|
||||||
|
.map(item -> ObjectUtil.defaultIfNull(item, 0.0))
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
if (averageRoe >= 35) {
|
||||||
|
roeScore = 550;
|
||||||
|
} else if (averageRoe >= 30) {
|
||||||
|
roeScore = 500;
|
||||||
|
} else if (averageRoe >= 25) {
|
||||||
|
roeScore = 450;
|
||||||
|
} else if (averageRoe >= 20) {
|
||||||
|
roeScore = 400;
|
||||||
|
} else if (averageRoe >= 15) {
|
||||||
|
roeScore = 350;
|
||||||
|
} else if (averageRoe >= 10) {
|
||||||
|
roeScore = 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + roeScore);
|
||||||
|
|
||||||
|
var roaScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnAssets() == null)) {
|
||||||
|
var averageRoa = recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getReturnOnAssets)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
if (averageRoa >= 15) {
|
||||||
|
roaScore = 100;
|
||||||
|
} else if (averageRoa >= 11) {
|
||||||
|
roaScore = 80;
|
||||||
|
} else if (averageRoa >= 7) {
|
||||||
|
roaScore = 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + roaScore);
|
||||||
|
|
||||||
|
var netProfitScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) {
|
||||||
|
var averageNetProfit = recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getNetProfit)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
if (averageNetProfit >= 10000.0 * 10000000) {
|
||||||
|
netProfitScore = 150;
|
||||||
|
} else if (averageNetProfit >= 1000.0 * 10000000) {
|
||||||
|
netProfitScore = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + netProfitScore);
|
||||||
|
|
||||||
|
var cashScore = 0;
|
||||||
|
if (
|
||||||
|
ArrayUtil.isAllNotNull(latestIndicator.getTotalAssetsTurnover(), latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio())
|
||||||
|
&& (
|
||||||
|
latestIndicator.getTotalAssetsTurnover() > 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.1
|
||||||
|
|| latestIndicator.getTotalAssetsTurnover() <= 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.2
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
cashScore = 50;
|
||||||
|
}
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + cashScore);
|
||||||
|
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getDaysAccountsReceivableTurnover()) && latestIndicator.getDaysAccountsReceivableTurnover() <= 30) {
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + 20);
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getDaysInventoryTurnover()) && latestIndicator.getDaysInventoryTurnover() <= 30) {
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + 20);
|
||||||
|
}
|
||||||
|
if (ArrayUtil.isAllNotNull(latestIndicator.getDaysAccountsReceivableTurnover(), latestIndicator.getDaysInventoryTurnover())) {
|
||||||
|
if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 40) {
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + 20);
|
||||||
|
} else if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 60) {
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getOperatingGrossProfitMargin() == null)) {
|
||||||
|
var stat = new DescriptiveStatistics();
|
||||||
|
recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getOperatingGrossProfitMargin)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.forEach(stat::addValue);
|
||||||
|
if (stat.getStandardDeviation() <= 0.3) {
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var operatingSafeMarginScore = 0;
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getOperatingSafetyMarginRatio())) {
|
||||||
|
if (latestIndicator.getOperatingSafetyMarginRatio() >= 70) {
|
||||||
|
operatingSafeMarginScore = 50;
|
||||||
|
} else if (latestIndicator.getOperatingSafetyMarginRatio() >= 50) {
|
||||||
|
operatingSafeMarginScore = 30;
|
||||||
|
} else if (latestIndicator.getOperatingSafetyMarginRatio() >= 30) {
|
||||||
|
operatingSafeMarginScore = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + operatingSafeMarginScore);
|
||||||
|
|
||||||
|
var netProfitAscendingScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) {
|
||||||
|
if (recentIndicators.get(0).getNetProfit() > recentIndicators.get(1).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 30;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 30;
|
||||||
|
}
|
||||||
|
if (recentIndicators.get(1).getNetProfit() > recentIndicators.get(2).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 25;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(2).getNetProfit() > recentIndicators.get(3).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 20;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(3).getNetProfit() > recentIndicators.get(4).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 15;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + netProfitAscendingScore);
|
||||||
|
|
||||||
|
var cashAscendingScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getCashAndCashEquivalents() == null)) {
|
||||||
|
if (recentIndicators.get(0).getCashAndCashEquivalents() > recentIndicators.get(1).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 30;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(1).getCashAndCashEquivalents() > recentIndicators.get(2).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 25;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(2).getCashAndCashEquivalents() > recentIndicators.get(3).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 20;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(3).getCashAndCashEquivalents() > recentIndicators.get(4).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 15;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores.put(stock.getCode(), scores.get(stock.getCode()) + cashAscendingScore);
|
||||||
|
}
|
||||||
|
var first50 = scores.entrySet()
|
||||||
|
.stream()
|
||||||
|
.sorted((e1, e2) -> e2.getValue() - e1.getValue())
|
||||||
|
.limit(50)
|
||||||
|
.map(entry -> stocksMap.get(entry.getKey()))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
var collection = new StockCollection();
|
||||||
|
collection.setName(StrUtil.format("金字塔选股 ({})", LocalDate.now()));
|
||||||
|
collection.setDescription("");
|
||||||
|
collection.setStocks(first50);
|
||||||
|
stockCollectionRepository.save(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
|
|||||||
import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx'
|
import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx'
|
||||||
import StockCollectionList from './pages/stock/StockCollectionList.tsx'
|
import StockCollectionList from './pages/stock/StockCollectionList.tsx'
|
||||||
import TaskDetail from './pages/task/TaskDetail.tsx'
|
import TaskDetail from './pages/task/TaskDetail.tsx'
|
||||||
|
import StockCollectionDetail from './pages/stock/StockCollectionDetail.tsx'
|
||||||
|
|
||||||
const routes: RouteObject[] = [
|
const routes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
@@ -46,6 +47,10 @@ const routes: RouteObject[] = [
|
|||||||
path: 'list',
|
path: 'list',
|
||||||
Component: StockCollectionList,
|
Component: StockCollectionList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'detail/:id',
|
||||||
|
Component: StockCollectionDetail,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
30
leopard-web/src/pages/stock/StockCollectionDetail.tsx
Normal file
30
leopard-web/src/pages/stock/StockCollectionDetail.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react"
|
||||||
|
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, stockListColumns} from '../../util/amis.tsx'
|
||||||
|
import {useNavigate, useParams} from 'react-router'
|
||||||
|
|
||||||
|
function StockCollectionDetail() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const {id} = useParams()
|
||||||
|
return (
|
||||||
|
<div className="stock-collection-detail">
|
||||||
|
{amisRender(
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: '股票集详情',
|
||||||
|
initApi: `get:${commonInfo.baseUrl}/stock_collection/detail/${id}`,
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'crud',
|
||||||
|
source: '${stocks}',
|
||||||
|
...crudCommonOptions(),
|
||||||
|
...paginationTemplate(15, undefined, ['filter-toggler']),
|
||||||
|
columns: stockListColumns(navigate),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(StockCollectionDetail)
|
||||||
@@ -1,8 +1,71 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
|
||||||
|
import {useNavigate} from 'react-router'
|
||||||
|
|
||||||
function StockCollectionList() {
|
function StockCollectionList() {
|
||||||
|
const navigate = useNavigate()
|
||||||
return (
|
return (
|
||||||
<div className="stock-collection-list"></div>
|
<div className="stock-collection-list">
|
||||||
|
{amisRender(
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: '股票列表',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'crud',
|
||||||
|
api: {
|
||||||
|
method: 'get',
|
||||||
|
url: `${commonInfo.baseUrl}/stock_collection/list`,
|
||||||
|
},
|
||||||
|
...crudCommonOptions(),
|
||||||
|
...paginationTemplate(15, undefined, ['filter-toggler']),
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: '名称',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
label: '描述',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
label: '股票数量',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'operation',
|
||||||
|
label: '操作',
|
||||||
|
width: 100,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '详情',
|
||||||
|
level: 'link',
|
||||||
|
onEvent: {
|
||||||
|
click: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
actionType: 'custom',
|
||||||
|
// @ts-ignore
|
||||||
|
script: (context, action, event) => {
|
||||||
|
navigate(`/stock/collection/detail/${context.props.data['id']}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import {
|
|||||||
amisRender,
|
amisRender,
|
||||||
commonInfo,
|
commonInfo,
|
||||||
crudCommonOptions,
|
crudCommonOptions,
|
||||||
date,
|
|
||||||
paginationTemplate,
|
paginationTemplate,
|
||||||
remoteMappings,
|
|
||||||
remoteOptions,
|
remoteOptions,
|
||||||
|
stockListColumns,
|
||||||
} from '../../util/amis.tsx'
|
} from '../../util/amis.tsx'
|
||||||
import {useNavigate} from 'react-router'
|
import {useNavigate} from 'react-router'
|
||||||
|
|
||||||
@@ -97,65 +96,7 @@ function StockList() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
columns: [
|
columns: stockListColumns(navigate),
|
||||||
{
|
|
||||||
name: 'code',
|
|
||||||
label: '编号',
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: '简称',
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fullname',
|
|
||||||
label: '全名',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'market',
|
|
||||||
label: '市场',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
...remoteMappings('stock_market', 'market'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'industry',
|
|
||||||
label: '行业',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '上市日期',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
...date('listedDate'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'operation',
|
|
||||||
label: '操作',
|
|
||||||
width: 100,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '详情',
|
|
||||||
level: 'link',
|
|
||||||
onEvent: {
|
|
||||||
click: {
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
actionType: 'custom',
|
|
||||||
// @ts-ignore
|
|
||||||
script: (context, action, event) => {
|
|
||||||
navigate(`/stock/detail/${context.props.data['id']}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'amis/sdk/iconfont.css'
|
|||||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {isEqual} from 'es-toolkit'
|
import {isEqual} from 'es-toolkit'
|
||||||
|
import type {NavigateFunction} from 'react-router'
|
||||||
|
|
||||||
export const commonInfo = {
|
export const commonInfo = {
|
||||||
debug: isEqual(import.meta.env.MODE, 'development'),
|
debug: isEqual(import.meta.env.MODE, 'development'),
|
||||||
@@ -334,3 +335,65 @@ export function remoteMappings(name: string, field: string) {
|
|||||||
source: `get:${commonInfo.baseUrl}/constants/mappings/${name}/${field}`,
|
source: `get:${commonInfo.baseUrl}/constants/mappings/${name}/${field}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stockListColumns(navigate: NavigateFunction) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
label: '编号',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: '简称',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fullname',
|
||||||
|
label: '全名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'market',
|
||||||
|
label: '市场',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
...remoteMappings('stock_market', 'market'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'industry',
|
||||||
|
label: '行业',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '上市日期',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
...date('listedDate'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'operation',
|
||||||
|
label: '操作',
|
||||||
|
width: 100,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '详情',
|
||||||
|
level: 'link',
|
||||||
|
onEvent: {
|
||||||
|
click: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
actionType: 'custom',
|
||||||
|
// @ts-ignore
|
||||||
|
script: (context, action, event) => {
|
||||||
|
navigate(`/stock/detail/${context.props.data['id']}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
11
pom.xml
11
pom.xml
@@ -81,6 +81,17 @@
|
|||||||
<version>${hibernate.version}</version>
|
<version>${hibernate.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
||||||
|
<artifactId>quantlib</artifactId>
|
||||||
|
<version>1.39.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ta4j</groupId>
|
||||||
|
<artifactId>ta4j-core</artifactId>
|
||||||
|
<version>0.17</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-dependencies</artifactId>
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
|||||||
Reference in New Issue
Block a user