feat: use tencent cos for uploads and add aslan stats
This commit is contained in:
parent
52658d5d65
commit
24e1af1841
@ -289,6 +289,7 @@
|
||||
|
||||
<tencentcloud-cls-logback-appender.version>1.0.4</tencentcloud-cls-logback-appender.version>
|
||||
<tencentcloud.version>3.1.877</tencentcloud.version>
|
||||
<cos_api.version>5.6.151</cos_api.version>
|
||||
<opentelemetry-logback.version>1.9.2-alpha</opentelemetry-logback.version>
|
||||
<protobuf-java.version>3.5.1</protobuf-java.version>
|
||||
<easyexcel.version>3.1.3</easyexcel.version>
|
||||
|
||||
@ -36,3 +36,13 @@ management:
|
||||
enabled: true
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
gateway:
|
||||
ignorePaths:
|
||||
- /console/account/login
|
||||
- /console/actuator/**
|
||||
- /console/datav/active/user-country-code
|
||||
- /console/datav/online/user/count
|
||||
- /console/datav/online/room/count
|
||||
- /console/datav/aslan/region-country/statistics
|
||||
- /console/user/base/info/im/sig
|
||||
|
||||
@ -2,7 +2,11 @@ package com.red.circle.external.inner.endpoint.oss.api;
|
||||
|
||||
import com.red.circle.framework.dto.ResultResponse;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* oss 服务api.
|
||||
@ -19,6 +23,13 @@ public interface OssServiceClientApi {
|
||||
@GetMapping("/sts")
|
||||
ResultResponse<Object> sts();
|
||||
|
||||
/**
|
||||
* 上传文件.
|
||||
*/
|
||||
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
ResultResponse<String> upload(@RequestPart("file") MultipartFile file,
|
||||
@RequestParam("key") String key);
|
||||
|
||||
/**
|
||||
* 移除文件.
|
||||
*/
|
||||
|
||||
@ -2,15 +2,21 @@ package com.red.circle.console.adapter.app.party3rd;//package com.sugartime.app.
|
||||
|
||||
import com.red.circle.external.inner.endpoint.oss.OssServiceClient;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 阿里云服务.
|
||||
* 对象存储服务.
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@ -21,11 +27,25 @@ public class AliYunRestController {
|
||||
private final OssServiceClient ossServiceClient;
|
||||
|
||||
/**
|
||||
* 阿里云sts.
|
||||
* 对象存储上传配置.
|
||||
*/
|
||||
@GetMapping("/oss/sts")
|
||||
public Object sts() {
|
||||
return ResponseAssert.requiredSuccess(ossServiceClient.sts());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件.
|
||||
*/
|
||||
@PostMapping(value = "/oss/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Map<String, Object> upload(@RequestPart("file") MultipartFile file,
|
||||
@RequestParam("key") String key) {
|
||||
String url = ResponseAssert.requiredSuccess(ossServiceClient.upload(file, key));
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("name", key);
|
||||
result.put("url", url);
|
||||
result.put("res", Map.of("status", 200));
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
package com.red.circle.console.adapter.app.user;
|
||||
|
||||
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
|
||||
import com.red.circle.console.app.dto.clienobject.count.aslan.AslanRegionCountryStatisticsCO;
|
||||
import com.red.circle.console.app.service.app.count.AslanRegionCountryStatisticsService;
|
||||
import com.red.circle.console.app.service.app.user.DatavService;
|
||||
import com.red.circle.framework.web.controller.BaseController;
|
||||
import com.red.circle.other.inner.model.cmd.user.OnlineCountQryCmd;
|
||||
import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@ -25,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class DatavRestController extends BaseController {
|
||||
|
||||
private final DatavService userExpandService;
|
||||
private final AslanRegionCountryStatisticsService aslanRegionCountryStatisticsService;
|
||||
|
||||
@GetMapping("/active/user-country-code")
|
||||
public List<ActiveUserCountryCodeDTO> latestActiveUserCountryCode() {
|
||||
@ -41,4 +46,11 @@ public class DatavRestController extends BaseController {
|
||||
return Objects.toString(userExpandService.count(sysOrigin));
|
||||
}
|
||||
|
||||
@GetMapping("/aslan/region-country/statistics")
|
||||
public List<AslanRegionCountryStatisticsCO> aslanRegionCountryStatistics(
|
||||
@RequestParam(value = "date", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
||||
return aslanRegionCountryStatisticsService.listRegionCountryStatistics(date);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,367 @@
|
||||
package com.red.circle.console.app.service.app.count;
|
||||
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.group;
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.match;
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation;
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.project;
|
||||
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
|
||||
import com.red.circle.console.app.dto.clienobject.count.aslan.AslanCountryStatisticsCO;
|
||||
import com.red.circle.console.app.dto.clienobject.count.aslan.AslanRegionCountryStatisticsCO;
|
||||
import com.red.circle.console.app.service.app.sys.SysCountryCodeService;
|
||||
import com.red.circle.console.infra.database.rds.dao.count.AslanRegionCountryStatisticsDAO;
|
||||
import com.red.circle.console.infra.database.rds.dto.count.CountryAmountMetricDTO;
|
||||
import com.red.circle.console.infra.database.rds.dto.count.CountryLongMetricDTO;
|
||||
import com.red.circle.console.infra.database.rds.dto.count.UserCountryMetricDTO;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.other.inner.endpoint.user.region.RegionConfigClient;
|
||||
import com.red.circle.other.inner.model.cmd.user.SysRegionConfigQryCmd;
|
||||
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
|
||||
import com.red.circle.other.inner.model.dto.user.reigon.RegionConfigDTO;
|
||||
import com.red.circle.tool.core.collection.CollectionUtils;
|
||||
import com.red.circle.tool.core.text.StringUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Aslan 区域国家统计.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AslanRegionCountryStatisticsServiceImpl implements AslanRegionCountryStatisticsService {
|
||||
|
||||
private static final String DAILY_ACTIVE_COLLECTION = "user_daily_active_log";
|
||||
private static final String TEAM_MEMBER_COLLECTION = "team_member";
|
||||
private static final String BANK_BALANCE_COLLECTION = "user_bank_balance";
|
||||
private static final Pattern COUNTRY_CODE_PATTERN = Pattern.compile("[A-Za-z0-9_]+");
|
||||
private static final ZoneId ASIA_SHANGHAI = ZoneId.of("Asia/Shanghai");
|
||||
private static final DateTimeFormatter DATE_NUMBER_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
|
||||
private static final int USER_ID_CHUNK_SIZE = 1000;
|
||||
|
||||
private final MongoTemplate mongoTemplate;
|
||||
private final RegionConfigClient regionConfigClient;
|
||||
private final SysCountryCodeService sysCountryCodeService;
|
||||
private final AslanRegionCountryStatisticsDAO statisticsDAO;
|
||||
|
||||
@Override
|
||||
public List<AslanRegionCountryStatisticsCO> listRegionCountryStatistics(LocalDate date) {
|
||||
LocalDate statDate = Objects.requireNonNullElseGet(date,
|
||||
() -> LocalDate.now(ASIA_SHANGHAI));
|
||||
String sysOrigin = SysOriginPlatformEnum.ATYOU.name();
|
||||
var startTime = statDate.atStartOfDay();
|
||||
var endTime = statDate.plusDays(1).atStartOfDay();
|
||||
Integer dateNumber = Integer.parseInt(statDate.format(DATE_NUMBER_FORMATTER));
|
||||
|
||||
List<RegionConfigDTO> regions = listRegionConfigs(sysOrigin);
|
||||
if (CollectionUtils.isEmpty(regions)) {
|
||||
return CollectionUtils.newArrayList();
|
||||
}
|
||||
|
||||
Map<String, SysCountryCodeDTO> countryMap = mapCountryCode();
|
||||
Map<String, BigDecimal> rechargeMap = mapAmount(
|
||||
statisticsDAO.listDailyRechargeByCountry(sysOrigin, dateNumber, startTime, endTime));
|
||||
Map<String, Long> registerMap = mapLong(
|
||||
statisticsDAO.listDailyRegisterByCountry(sysOrigin, startTime, endTime));
|
||||
Map<String, Long> goldBalanceMap = mapLong(statisticsDAO.listGoldBalanceByCountry(sysOrigin));
|
||||
Map<String, Long> dailyActiveMap = mapDailyActive(sysOrigin, statDate);
|
||||
Map<String, BigDecimal> hostSalaryBalanceMap = mapHostSalaryBalance(sysOrigin);
|
||||
|
||||
return regions.stream()
|
||||
.sorted(Comparator.comparing(RegionConfigDTO::getCreateTime,
|
||||
Comparator.nullsLast(Comparator.naturalOrder())).reversed())
|
||||
.map(region -> buildRegionStatistics(statDate, region, countryMap, rechargeMap,
|
||||
registerMap, goldBalanceMap, dailyActiveMap, hostSalaryBalanceMap))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<RegionConfigDTO> listRegionConfigs(String sysOrigin) {
|
||||
SysRegionConfigQryCmd query = new SysRegionConfigQryCmd();
|
||||
query.setSysOrigin(sysOrigin);
|
||||
return Optional.ofNullable(ResponseAssert.requiredSuccess(regionConfigClient.listByCondition(query)))
|
||||
.orElseGet(CollectionUtils::newArrayList);
|
||||
}
|
||||
|
||||
private Map<String, SysCountryCodeDTO> mapCountryCode() {
|
||||
List<SysCountryCodeDTO> countries = sysCountryCodeService.listContent();
|
||||
if (CollectionUtils.isEmpty(countries)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
return countries.stream()
|
||||
.filter(country -> StringUtils.isNotBlank(country.getAlphaTwo()))
|
||||
.collect(Collectors.toMap(
|
||||
country -> normalizeCountryCode(country.getAlphaTwo()),
|
||||
Function.identity(),
|
||||
(left, right) -> left));
|
||||
}
|
||||
|
||||
private AslanRegionCountryStatisticsCO buildRegionStatistics(
|
||||
LocalDate statDate,
|
||||
RegionConfigDTO region,
|
||||
Map<String, SysCountryCodeDTO> countryMap,
|
||||
Map<String, BigDecimal> rechargeMap,
|
||||
Map<String, Long> registerMap,
|
||||
Map<String, Long> goldBalanceMap,
|
||||
Map<String, Long> dailyActiveMap,
|
||||
Map<String, BigDecimal> hostSalaryBalanceMap) {
|
||||
|
||||
List<AslanCountryStatisticsCO> countries = parseCountryCodes(region.getCountryCodes()).stream()
|
||||
.map(countryCode -> buildCountryStatistics(countryCode, countryMap, rechargeMap,
|
||||
registerMap, goldBalanceMap, dailyActiveMap, hostSalaryBalanceMap))
|
||||
.toList();
|
||||
|
||||
BigDecimal regionRecharge = countries.stream()
|
||||
.map(AslanCountryStatisticsCO::getDailyRecharge)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
Long regionActive = countries.stream().mapToLong(AslanCountryStatisticsCO::getDailyActive).sum();
|
||||
Long regionRegister = countries.stream().mapToLong(AslanCountryStatisticsCO::getDailyRegister)
|
||||
.sum();
|
||||
Long regionGoldBalance = countries.stream().mapToLong(AslanCountryStatisticsCO::getGoldBalance)
|
||||
.sum();
|
||||
BigDecimal regionHostSalaryBalance = countries.stream()
|
||||
.map(AslanCountryStatisticsCO::getHostSalaryBalance)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
return new AslanRegionCountryStatisticsCO()
|
||||
.setDate(statDate.toString())
|
||||
.setRegionId(region.getId())
|
||||
.setRegionCode(region.getRegionCode())
|
||||
.setRegionName(region.getRegionName())
|
||||
.setDailyRecharge(regionRecharge)
|
||||
.setDailyActive(regionActive)
|
||||
.setDailyRegister(regionRegister)
|
||||
.setGoldBalance(regionGoldBalance)
|
||||
.setHostSalaryBalance(regionHostSalaryBalance)
|
||||
.setCountries(countries);
|
||||
}
|
||||
|
||||
private AslanCountryStatisticsCO buildCountryStatistics(
|
||||
String countryCode,
|
||||
Map<String, SysCountryCodeDTO> countryMap,
|
||||
Map<String, BigDecimal> rechargeMap,
|
||||
Map<String, Long> registerMap,
|
||||
Map<String, Long> goldBalanceMap,
|
||||
Map<String, Long> dailyActiveMap,
|
||||
Map<String, BigDecimal> hostSalaryBalanceMap) {
|
||||
|
||||
SysCountryCodeDTO country = countryMap.get(countryCode);
|
||||
return new AslanCountryStatisticsCO()
|
||||
.setCountryCode(countryCode)
|
||||
.setCountryName(Optional.ofNullable(country)
|
||||
.map(SysCountryCodeDTO::getCountryName)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElse(countryCode))
|
||||
.setDailyRecharge(rechargeMap.getOrDefault(countryCode, BigDecimal.ZERO))
|
||||
.setDailyActive(dailyActiveMap.getOrDefault(countryCode, 0L))
|
||||
.setDailyRegister(registerMap.getOrDefault(countryCode, 0L))
|
||||
.setGoldBalance(goldBalanceMap.getOrDefault(countryCode, 0L))
|
||||
.setHostSalaryBalance(hostSalaryBalanceMap.getOrDefault(countryCode, BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> mapAmount(List<CountryAmountMetricDTO> metrics) {
|
||||
if (CollectionUtils.isEmpty(metrics)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
return metrics.stream()
|
||||
.filter(metric -> StringUtils.isNotBlank(metric.getCountryCode()))
|
||||
.collect(Collectors.toMap(
|
||||
metric -> normalizeCountryCode(metric.getCountryCode()),
|
||||
metric -> Optional.ofNullable(metric.getAmount()).orElse(BigDecimal.ZERO),
|
||||
BigDecimal::add));
|
||||
}
|
||||
|
||||
private Map<String, Long> mapLong(List<CountryLongMetricDTO> metrics) {
|
||||
if (CollectionUtils.isEmpty(metrics)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
return metrics.stream()
|
||||
.filter(metric -> StringUtils.isNotBlank(metric.getCountryCode()))
|
||||
.collect(Collectors.toMap(
|
||||
metric -> normalizeCountryCode(metric.getCountryCode()),
|
||||
metric -> Optional.ofNullable(metric.getQuantity()).orElse(0L),
|
||||
Long::sum));
|
||||
}
|
||||
|
||||
private Map<String, Long> mapDailyActive(String sysOrigin, LocalDate statDate) {
|
||||
var aggregation = newAggregation(
|
||||
match(Criteria.where("activeDate").is(statDate.toString()).and("sysOrigin").is(sysOrigin)),
|
||||
group("registerCountryCode").count().as("quantity"),
|
||||
project("quantity").and("_id").as("countryCode")
|
||||
);
|
||||
|
||||
var results = mongoTemplate.aggregate(aggregation, DAILY_ACTIVE_COLLECTION, BasicDBObject.class)
|
||||
.getMappedResults();
|
||||
if (CollectionUtils.isEmpty(results)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
|
||||
return results.stream()
|
||||
.filter(result -> StringUtils.isNotBlank(result.getString("countryCode")))
|
||||
.collect(Collectors.toMap(
|
||||
result -> normalizeCountryCode(result.getString("countryCode")),
|
||||
result -> Optional.ofNullable(result.get("quantity"))
|
||||
.map(Number.class::cast)
|
||||
.map(Number::longValue)
|
||||
.orElse(0L),
|
||||
Long::sum));
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> mapHostSalaryBalance(String sysOrigin) {
|
||||
Set<Long> anchorUserIds = listAnchorUserIds(sysOrigin);
|
||||
if (CollectionUtils.isEmpty(anchorUserIds)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
|
||||
Map<Long, Long> bankBalanceMap = mapBankBalance(sysOrigin, anchorUserIds);
|
||||
if (CollectionUtils.isEmpty(bankBalanceMap)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
|
||||
Map<Long, String> userCountryMap = mapUserCountry(sysOrigin, bankBalanceMap.keySet());
|
||||
if (CollectionUtils.isEmpty(userCountryMap)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
|
||||
Map<String, BigDecimal> result = CollectionUtils.newHashMap();
|
||||
bankBalanceMap.forEach((userId, balance) -> {
|
||||
String countryCode = userCountryMap.get(userId);
|
||||
if (StringUtils.isBlank(countryCode)) {
|
||||
return;
|
||||
}
|
||||
result.merge(countryCode, pennyToDollar(balance), BigDecimal::add);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<Long> listAnchorUserIds(String sysOrigin) {
|
||||
Query query = Query.query(Criteria.where("sysOrigin").is(sysOrigin)
|
||||
.and("role").in("MEMBER", "ADMIN"));
|
||||
query.fields().include("memberId");
|
||||
|
||||
List<BasicDBObject> members = mongoTemplate.find(query, BasicDBObject.class,
|
||||
TEAM_MEMBER_COLLECTION);
|
||||
if (CollectionUtils.isEmpty(members)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
return members.stream()
|
||||
.map(member -> numberToLong(member.get("memberId")))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private Map<Long, Long> mapBankBalance(String sysOrigin, Set<Long> userIds) {
|
||||
Map<Long, Long> result = CollectionUtils.newHashMap();
|
||||
for (List<Long> chunk : partitionUserIds(userIds)) {
|
||||
Query query = Query.query(Criteria.where("sysOrigin").is(sysOrigin).and("_id").in(chunk));
|
||||
query.fields().include("_id").include("balance");
|
||||
|
||||
List<BasicDBObject> balances = mongoTemplate.find(query, BasicDBObject.class,
|
||||
BANK_BALANCE_COLLECTION);
|
||||
if (CollectionUtils.isEmpty(balances)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
balances.forEach(balance -> {
|
||||
Long userId = numberToLong(balance.get("_id"));
|
||||
Long amount = numberToLong(balance.get("balance"));
|
||||
if (Objects.nonNull(userId) && Objects.nonNull(amount)) {
|
||||
result.merge(userId, amount, Long::sum);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Long, String> mapUserCountry(String sysOrigin, Set<Long> userIds) {
|
||||
Map<Long, String> result = CollectionUtils.newHashMap();
|
||||
for (List<Long> chunk : partitionUserIds(userIds)) {
|
||||
List<UserCountryMetricDTO> users = statisticsDAO.listUserCountryByIds(sysOrigin, chunk);
|
||||
if (CollectionUtils.isEmpty(users)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
users.stream()
|
||||
.filter(user -> Objects.nonNull(user.getUserId()))
|
||||
.filter(user -> StringUtils.isNotBlank(user.getCountryCode()))
|
||||
.forEach(user -> result.put(user.getUserId(), normalizeCountryCode(user.getCountryCode())));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<List<Long>> partitionUserIds(Set<Long> userIds) {
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Long> ids = userIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
List<List<Long>> partitions = new ArrayList<>();
|
||||
for (int index = 0; index < ids.size(); index += USER_ID_CHUNK_SIZE) {
|
||||
partitions.add(ids.subList(index, Math.min(index + USER_ID_CHUNK_SIZE, ids.size())));
|
||||
}
|
||||
return partitions;
|
||||
}
|
||||
|
||||
private BigDecimal pennyToDollar(Long pennyAmount) {
|
||||
if (Objects.isNull(pennyAmount)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return BigDecimal.valueOf(pennyAmount).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
|
||||
}
|
||||
|
||||
private Long numberToLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (Objects.isNull(value) || StringUtils.isBlank(value.toString())) {
|
||||
return null;
|
||||
}
|
||||
return Long.valueOf(value.toString());
|
||||
}
|
||||
|
||||
private Set<String> parseCountryCodes(String countryCodes) {
|
||||
if (StringUtils.isBlank(countryCodes)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
Matcher matcher = COUNTRY_CODE_PATTERN.matcher(countryCodes);
|
||||
while (matcher.find()) {
|
||||
String code = normalizeCountryCode(matcher.group());
|
||||
if (StringUtils.isNotBlank(code)) {
|
||||
result.add(code);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(result);
|
||||
}
|
||||
|
||||
private String normalizeCountryCode(String countryCode) {
|
||||
return StringUtils.isBlank(countryCode)
|
||||
? ""
|
||||
: countryCode.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.red.circle.console.app.dto.clienobject.count.aslan;
|
||||
|
||||
import com.red.circle.framework.dto.DTO;
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Aslan 国家统计.
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public class AslanCountryStatisticsCO extends DTO {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 国家编码.
|
||||
*/
|
||||
private String countryCode;
|
||||
|
||||
/**
|
||||
* 国家名称.
|
||||
*/
|
||||
private String countryName;
|
||||
|
||||
/**
|
||||
* 每日充值.
|
||||
*/
|
||||
private BigDecimal dailyRecharge;
|
||||
|
||||
/**
|
||||
* 日活.
|
||||
*/
|
||||
private Long dailyActive;
|
||||
|
||||
/**
|
||||
* 每日新注册.
|
||||
*/
|
||||
private Long dailyRegister;
|
||||
|
||||
/**
|
||||
* 当前金币总量.
|
||||
*/
|
||||
private Long goldBalance;
|
||||
|
||||
/**
|
||||
* 主播当前工资余额.
|
||||
*/
|
||||
private BigDecimal hostSalaryBalance;
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.red.circle.console.app.dto.clienobject.count.aslan;
|
||||
|
||||
import com.red.circle.framework.dto.DTO;
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Aslan 区域国家统计.
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public class AslanRegionCountryStatisticsCO extends DTO {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 统计日期.
|
||||
*/
|
||||
private String date;
|
||||
|
||||
/**
|
||||
* 区域ID.
|
||||
*/
|
||||
private String regionId;
|
||||
|
||||
/**
|
||||
* 区域编码.
|
||||
*/
|
||||
private String regionCode;
|
||||
|
||||
/**
|
||||
* 区域名称.
|
||||
*/
|
||||
private String regionName;
|
||||
|
||||
/**
|
||||
* 区域每日充值合计.
|
||||
*/
|
||||
private BigDecimal dailyRecharge;
|
||||
|
||||
/**
|
||||
* 区域日活合计.
|
||||
*/
|
||||
private Long dailyActive;
|
||||
|
||||
/**
|
||||
* 区域每日新注册合计.
|
||||
*/
|
||||
private Long dailyRegister;
|
||||
|
||||
/**
|
||||
* 区域当前金币总量.
|
||||
*/
|
||||
private Long goldBalance;
|
||||
|
||||
/**
|
||||
* 区域主播当前工资余额.
|
||||
*/
|
||||
private BigDecimal hostSalaryBalance;
|
||||
|
||||
/**
|
||||
* 区域下国家统计.
|
||||
*/
|
||||
private List<AslanCountryStatisticsCO> countries;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.red.circle.console.app.service.app.count;
|
||||
|
||||
import com.red.circle.console.app.dto.clienobject.count.aslan.AslanRegionCountryStatisticsCO;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Aslan 区域国家统计.
|
||||
*/
|
||||
public interface AslanRegionCountryStatisticsService {
|
||||
|
||||
/**
|
||||
* 获取指定日期的区域国家聚合数据.
|
||||
*/
|
||||
List<AslanRegionCountryStatisticsCO> listRegionCountryStatistics(LocalDate date);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.red.circle.console.infra.database.rds.dao.count;
|
||||
|
||||
import com.red.circle.console.infra.database.rds.dto.count.CountryAmountMetricDTO;
|
||||
import com.red.circle.console.infra.database.rds.dto.count.CountryLongMetricDTO;
|
||||
import com.red.circle.console.infra.database.rds.dto.count.UserCountryMetricDTO;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* Aslan 区域国家统计 Mapper.
|
||||
*/
|
||||
@Mapper
|
||||
public interface AslanRegionCountryStatisticsDAO {
|
||||
|
||||
/**
|
||||
* 每日充值,包含订单、后台离线充值和币商充值.
|
||||
*/
|
||||
List<CountryAmountMetricDTO> listDailyRechargeByCountry(
|
||||
@Param("sysOrigin") String sysOrigin,
|
||||
@Param("dateNumber") Integer dateNumber,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 每日新注册.
|
||||
*/
|
||||
List<CountryLongMetricDTO> listDailyRegisterByCountry(
|
||||
@Param("sysOrigin") String sysOrigin,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 当前金币总量.
|
||||
*/
|
||||
List<CountryLongMetricDTO> listGoldBalanceByCountry(@Param("sysOrigin") String sysOrigin);
|
||||
|
||||
/**
|
||||
* 批量查询用户国家.
|
||||
*/
|
||||
List<UserCountryMetricDTO> listUserCountryByIds(
|
||||
@Param("sysOrigin") String sysOrigin,
|
||||
@Param("userIds") List<Long> userIds);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.red.circle.console.infra.database.rds.dto.count;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 国家金额指标.
|
||||
*/
|
||||
@Data
|
||||
public class CountryAmountMetricDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 国家编码.
|
||||
*/
|
||||
private String countryCode;
|
||||
|
||||
/**
|
||||
* 金额.
|
||||
*/
|
||||
private BigDecimal amount;
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.red.circle.console.infra.database.rds.dto.count;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 国家数值指标.
|
||||
*/
|
||||
@Data
|
||||
public class CountryLongMetricDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 国家编码.
|
||||
*/
|
||||
private String countryCode;
|
||||
|
||||
/**
|
||||
* 数量.
|
||||
*/
|
||||
private Long quantity;
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.red.circle.console.infra.database.rds.dto.count;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 用户国家指标.
|
||||
*/
|
||||
@Data
|
||||
public class UserCountryMetricDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID.
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 国家编码.
|
||||
*/
|
||||
private String countryCode;
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper
|
||||
namespace="com.red.circle.console.infra.database.rds.dao.count.AslanRegionCountryStatisticsDAO">
|
||||
|
||||
<select id="listDailyRechargeByCountry"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.count.CountryAmountMetricDTO">
|
||||
SELECT countryCode,
|
||||
SUM(amount) AS amount
|
||||
FROM (
|
||||
SELECT UPPER(ubi.country_code) AS countryCode,
|
||||
SUM(oph.unit_price) AS amount
|
||||
FROM order_purchase_history oph
|
||||
INNER JOIN user_base_info ubi ON ubi.id = oph.user_id
|
||||
WHERE oph.sys_origin = #{sysOrigin}
|
||||
AND oph.evn = 'PROD'
|
||||
AND oph.status = 'COMPLETE'
|
||||
AND IFNULL(oph.is_trial_period, 0) = 0
|
||||
AND oph.update_time >= #{startTime}
|
||||
AND oph.update_time < #{endTime}
|
||||
GROUP BY UPPER(ubi.country_code)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT UPPER(ubi.country_code) AS countryCode,
|
||||
SUM(oor.amount) AS amount
|
||||
FROM order_other_recharge oor
|
||||
INNER JOIN user_base_info ubi ON ubi.id = oor.user_Id
|
||||
WHERE oor.sys_origin = #{sysOrigin}
|
||||
AND oor.payment_date_number = #{dateNumber}
|
||||
GROUP BY UPPER(ubi.country_code)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT UPPER(ubi.country_code) AS countryCode,
|
||||
SUM(ugcr.recharge_amount) AS amount
|
||||
FROM user_gold_coin_recharge ugcr
|
||||
INNER JOIN user_base_info ubi ON ubi.id = ugcr.user_id
|
||||
WHERE ugcr.sys_origin = #{sysOrigin}
|
||||
AND ugcr.gold_coin_source = 2
|
||||
AND ugcr.create_time >= #{startTime}
|
||||
AND ugcr.create_time < #{endTime}
|
||||
GROUP BY UPPER(ubi.country_code)
|
||||
) recharge
|
||||
WHERE countryCode IS NOT NULL
|
||||
AND countryCode != ''
|
||||
GROUP BY countryCode
|
||||
</select>
|
||||
|
||||
<select id="listDailyRegisterByCountry"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.count.CountryLongMetricDTO">
|
||||
SELECT UPPER(country_code) AS countryCode,
|
||||
COUNT(1) AS quantity
|
||||
FROM user_base_info
|
||||
WHERE origin_sys = #{sysOrigin}
|
||||
AND create_time >= #{startTime}
|
||||
AND create_time < #{endTime}
|
||||
AND country_code IS NOT NULL
|
||||
AND country_code != ''
|
||||
GROUP BY UPPER(country_code)
|
||||
</select>
|
||||
|
||||
<select id="listGoldBalanceByCountry"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.count.CountryLongMetricDTO">
|
||||
SELECT UPPER(ubi.country_code) AS countryCode,
|
||||
SUM(GREATEST(wgb.income - wgb.expenditure, 0)) AS quantity
|
||||
FROM wallet_gold_balance wgb
|
||||
INNER JOIN user_base_info ubi ON ubi.id = wgb.id
|
||||
WHERE wgb.sys_origin = #{sysOrigin}
|
||||
AND ubi.origin_sys = #{sysOrigin}
|
||||
AND ubi.country_code IS NOT NULL
|
||||
AND ubi.country_code != ''
|
||||
GROUP BY UPPER(ubi.country_code)
|
||||
</select>
|
||||
|
||||
<select id="listUserCountryByIds"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.count.UserCountryMetricDTO">
|
||||
SELECT id AS userId,
|
||||
UPPER(country_code) AS countryCode
|
||||
FROM user_base_info
|
||||
WHERE origin_sys = #{sysOrigin}
|
||||
AND country_code IS NOT NULL
|
||||
AND country_code != ''
|
||||
AND id IN
|
||||
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
|
||||
#{userId}
|
||||
</foreach>
|
||||
</select>
|
||||
</mapper>
|
||||
@ -69,4 +69,5 @@ red-circle:
|
||||
- /console/datav/active/user-country-code
|
||||
- /console/datav/online/user/count
|
||||
- /console/datav/online/room/count
|
||||
- /console/datav/aslan/region-country/statistics
|
||||
- /console/user/base/info/im/sig
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
package com.red.circle.external.adapter.app.oss;
|
||||
|
||||
import com.red.circle.component.oss.OssService;
|
||||
import com.red.circle.component.oss.aliyun.service.AliYunStsResponse;
|
||||
import com.red.circle.component.oss.aliyun.service.AliYunStsService;
|
||||
import com.red.circle.external.infra.oss.TencentCosCredentialService;
|
||||
import com.red.circle.external.response.OssErrorCode;
|
||||
import com.red.circle.external.utils.FileUtils;
|
||||
import com.red.circle.framework.dto.ResultResponse;
|
||||
@ -13,11 +12,10 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 阿里云,OSS存储.
|
||||
* 对象存储.
|
||||
*
|
||||
* @author pengliang on 2020/9/7
|
||||
* @eo.api-type http
|
||||
@ -30,11 +28,11 @@ import java.util.UUID;
|
||||
@RequestMapping(value = "/oss", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class OssRestController {
|
||||
|
||||
private final AliYunStsService aliYunStsService;
|
||||
private final TencentCosCredentialService tencentCosCredentialService;
|
||||
private final OssService ossService;
|
||||
|
||||
/**
|
||||
* sts临时令牌.
|
||||
* COS 上传配置.
|
||||
*
|
||||
* @eo.name sts临时令牌.
|
||||
* @eo.url /sts
|
||||
@ -43,8 +41,8 @@ public class OssRestController {
|
||||
*/
|
||||
@IgnoreResultResponse
|
||||
@GetMapping("/sts")
|
||||
public AliYunStsResponse sts() {
|
||||
return aliYunStsService.getAcsResponse();
|
||||
public Object sts() {
|
||||
return tencentCosCredentialService.getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -36,6 +36,12 @@
|
||||
<artifactId>component-oss</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.qcloud</groupId>
|
||||
<artifactId>cos_api</artifactId>
|
||||
<version>${cos_api.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.red.circle</groupId>
|
||||
<artifactId>component-censor</artifactId>
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package com.red.circle.external.infra.oss;
|
||||
|
||||
import com.red.circle.external.infra.props.TencentCosProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* COS 配置信息服务.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TencentCosCredentialService {
|
||||
|
||||
private final TencentCosProperties properties;
|
||||
|
||||
public TencentCosStsResponse getResponse() {
|
||||
return TencentCosStsResponse.builder()
|
||||
.provider("tencent-cos")
|
||||
.bucketName(properties.getBucketName())
|
||||
.region(properties.getRegion())
|
||||
.accessUrl(properties.normalizedAccessUrl())
|
||||
.uploadMode("server")
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
package com.red.circle.external.infra.oss;
|
||||
|
||||
import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.ClientConfig;
|
||||
import com.qcloud.cos.auth.BasicCOSCredentials;
|
||||
import com.qcloud.cos.auth.COSCredentials;
|
||||
import com.qcloud.cos.exception.CosClientException;
|
||||
import com.qcloud.cos.model.COSObject;
|
||||
import com.qcloud.cos.model.CopyObjectRequest;
|
||||
import com.qcloud.cos.model.ObjectMetadata;
|
||||
import com.qcloud.cos.model.PutObjectRequest;
|
||||
import com.qcloud.cos.model.PutObjectResult;
|
||||
import com.qcloud.cos.model.ciModel.common.ImageProcessRequest;
|
||||
import com.qcloud.cos.model.ciModel.persistence.PicOperations;
|
||||
import com.qcloud.cos.region.Region;
|
||||
import com.red.circle.component.oss.OssService;
|
||||
import com.red.circle.external.infra.props.TencentCosProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 腾讯云 COS 对象存储实现.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("ossService")
|
||||
@RequiredArgsConstructor
|
||||
public class TencentCosOssService implements OssService {
|
||||
|
||||
private static final String VIDEO_COVER_PROCESS = "?ci-process=snapshot&time=1&format=jpg";
|
||||
private static final String OSS_RESIZE_PREFIX = "image/resize,m_lfit,h_";
|
||||
private static final String COS_IMAGE_RULE_PREFIX = "imageMogr2/";
|
||||
|
||||
private final TencentCosProperties properties;
|
||||
private COSClient cosClient;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
properties.validate();
|
||||
COSCredentials credentials = new BasicCOSCredentials(properties.getSecretId(), properties.getSecretKey());
|
||||
ClientConfig clientConfig = new ClientConfig(new Region(properties.getRegion()));
|
||||
clientConfig.setConnectionTimeout(10_000);
|
||||
clientConfig.setSocketTimeout(30_000);
|
||||
this.cosClient = new COSClient(credentials, clientConfig);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
if (cosClient != null) {
|
||||
cosClient.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadContent(String content, String fileName) {
|
||||
byte[] bytes = StringUtils.defaultString(content).getBytes(StandardCharsets.UTF_8);
|
||||
uploadByte(bytes, fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadByte(byte[] bytes, String fileName) {
|
||||
byte[] safeBytes = bytes == null ? new byte[0] : bytes;
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(safeBytes.length);
|
||||
uploadInputStream(new ByteArrayInputStream(safeBytes), fileName, metadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadUrl(String url, String fileName) throws IOException {
|
||||
try (InputStream inputStream = new URL(url).openStream()) {
|
||||
uploadInputStream(inputStream, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadInputStream(InputStream inputStream, String fileName) {
|
||||
uploadInputStream(inputStream, fileName, new ObjectMetadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadFile(File file, String fileName) {
|
||||
String key = normalizeKey(fileName);
|
||||
PutObjectRequest request = new PutObjectRequest(properties.getBucketName(), key, file);
|
||||
cosClient.putObject(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String url) {
|
||||
String key = getFilePath(url);
|
||||
if (StringUtils.isBlank(key)) {
|
||||
return;
|
||||
}
|
||||
cosClient.deleteObject(properties.getBucketName(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessUrl(String key) {
|
||||
if (StringUtils.isBlank(key)) {
|
||||
return "";
|
||||
}
|
||||
if (isHttpUrl(key)) {
|
||||
return key;
|
||||
}
|
||||
return properties.normalizedAccessUrl() + "/" + normalizeKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVideoCover(String key) {
|
||||
String accessUrl = getAccessUrl(key);
|
||||
if (StringUtils.isBlank(accessUrl)) {
|
||||
return "";
|
||||
}
|
||||
return accessUrl + VIDEO_COVER_PROCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFileSaveAs(String source, String target, String styleType) {
|
||||
String sourceKey = getFilePath(source);
|
||||
String targetKey = normalizeKey(target);
|
||||
if (StringUtils.isAnyBlank(sourceKey, targetKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PicOperations picOperations = new PicOperations();
|
||||
picOperations.setIsPicInfo(1);
|
||||
PicOperations.Rule rule = new PicOperations.Rule();
|
||||
rule.setBucket(properties.getBucketName());
|
||||
rule.setFileId(targetKey);
|
||||
rule.setRule(toCosImageRule(styleType));
|
||||
picOperations.setRules(List.of(rule));
|
||||
|
||||
ImageProcessRequest request = new ImageProcessRequest(properties.getBucketName(), sourceKey);
|
||||
request.setPicOperations(picOperations);
|
||||
cosClient.processImage(request);
|
||||
} catch (CosClientException ex) {
|
||||
log.warn("COS image process failed, fallback to object copy. source={}, target={}, message={}",
|
||||
sourceKey, targetKey, ex.getMessage());
|
||||
copySourceToTarget(source, sourceKey, targetKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processImgSaveAsCompressZoom(String source, String target, int height) {
|
||||
if (isHttpUrl(source)) {
|
||||
return source;
|
||||
}
|
||||
if (StringUtils.endsWith(source, ".gift")) {
|
||||
return getAccessUrl(source);
|
||||
}
|
||||
processFileSaveAs(source, target, OSS_RESIZE_PREFIX + height);
|
||||
return getAccessUrl(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processImgSaveAsCompressZoom(String source, int height) {
|
||||
return processImgSaveAsCompressZoom(source, source, height);
|
||||
}
|
||||
|
||||
private void uploadInputStream(InputStream inputStream, String fileName, ObjectMetadata metadata) {
|
||||
String key = normalizeKey(fileName);
|
||||
PutObjectRequest request = new PutObjectRequest(properties.getBucketName(), key, inputStream, metadata);
|
||||
PutObjectResult result = cosClient.putObject(request);
|
||||
log.debug("COS upload success, key={}, requestId={}", key, result.getRequestId());
|
||||
}
|
||||
|
||||
private void copySourceToTarget(String source, String sourceKey, String targetKey) {
|
||||
if (isHttpUrl(source)) {
|
||||
try {
|
||||
uploadUrl(source, targetKey);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("copy remote object to COS failed", ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
CopyObjectRequest request = new CopyObjectRequest(properties.getBucketName(), sourceKey,
|
||||
properties.getBucketName(), targetKey);
|
||||
cosClient.copyObject(request);
|
||||
}
|
||||
|
||||
private String toCosImageRule(String styleType) {
|
||||
String style = StringUtils.trimToEmpty(styleType);
|
||||
if (StringUtils.startsWith(style, COS_IMAGE_RULE_PREFIX)) {
|
||||
return style;
|
||||
}
|
||||
if (StringUtils.startsWith(style, OSS_RESIZE_PREFIX)) {
|
||||
String height = StringUtils.substringAfter(style, OSS_RESIZE_PREFIX);
|
||||
if (StringUtils.isNumeric(height)) {
|
||||
return "imageMogr2/thumbnail/x" + height;
|
||||
}
|
||||
}
|
||||
return "imageMogr2/thumbnail/x1024";
|
||||
}
|
||||
|
||||
private String getFilePath(String urlOrKey) {
|
||||
if (StringUtils.isBlank(urlOrKey)) {
|
||||
return urlOrKey;
|
||||
}
|
||||
String value = StringUtils.trim(urlOrKey);
|
||||
if (!isHttpUrl(value)) {
|
||||
return normalizeKey(StringUtils.substringBefore(value, "?"));
|
||||
}
|
||||
URI uri = URI.create(value);
|
||||
return normalizeKey(uri.getPath());
|
||||
}
|
||||
|
||||
private String normalizeKey(String key) {
|
||||
return StringUtils.stripStart(StringUtils.substringBefore(StringUtils.trimToEmpty(key), "?"), "/");
|
||||
}
|
||||
|
||||
private boolean isHttpUrl(String value) {
|
||||
return StringUtils.startsWithAny(StringUtils.lowerCase(StringUtils.trimToEmpty(value)), "http://", "https://");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.red.circle.external.infra.oss;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* COS 存储公开配置响应.
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class TencentCosStsResponse {
|
||||
|
||||
String provider;
|
||||
String bucketName;
|
||||
String region;
|
||||
String accessUrl;
|
||||
String uploadMode;
|
||||
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.red.circle.external.infra.props;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 腾讯云 COS 上传配置.
|
||||
*/
|
||||
@Data
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Component
|
||||
@ConfigurationProperties(TencentCosProperties.PREFIX)
|
||||
public class TencentCosProperties {
|
||||
|
||||
public static final String PREFIX = "red-circle.oss.tencent-cos";
|
||||
|
||||
String secretId;
|
||||
String secretKey;
|
||||
String bucketName;
|
||||
String region;
|
||||
String accessUrl;
|
||||
|
||||
public void validate() {
|
||||
if (StringUtils.isAnyBlank(secretId, secretKey, bucketName, region, accessUrl)) {
|
||||
throw new IllegalStateException(PREFIX + " config is incomplete");
|
||||
}
|
||||
URI accessUri = URI.create(accessUrl.trim());
|
||||
String scheme = StringUtils.lowerCase(accessUri.getScheme(), Locale.ROOT);
|
||||
if (!StringUtils.equalsAny(scheme, "http", "https") || StringUtils.isBlank(accessUri.getHost())) {
|
||||
throw new IllegalStateException(PREFIX + ".access-url must be an absolute http url");
|
||||
}
|
||||
}
|
||||
|
||||
public String normalizedAccessUrl() {
|
||||
return StringUtils.stripEnd(StringUtils.trimToEmpty(accessUrl), "/");
|
||||
}
|
||||
|
||||
}
|
||||
@ -8,6 +8,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* oss 服务.
|
||||
@ -27,6 +28,11 @@ public class OssServiceClientEndpoint implements OssServiceClientApi {
|
||||
return ResultResponse.success(ossServiceClientService.sts());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<String> upload(MultipartFile file, String key) {
|
||||
return ResultResponse.success(ossServiceClientService.upload(file, key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> remove(String url) {
|
||||
ossServiceClientService.remove(url);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package com.red.circle.external.inner.service.oss;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* oss 服务api.
|
||||
*
|
||||
@ -12,6 +14,11 @@ public interface OssServiceClientService {
|
||||
*/
|
||||
Object sts();
|
||||
|
||||
/**
|
||||
* 上传文件.
|
||||
*/
|
||||
String upload(MultipartFile file, String key);
|
||||
|
||||
/**
|
||||
* 移除文件.
|
||||
*/
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
package com.red.circle.external.inner.service.oss.impl;
|
||||
|
||||
import com.red.circle.component.oss.OssService;
|
||||
import com.red.circle.component.oss.aliyun.service.AliYunStsService;
|
||||
import com.red.circle.external.infra.oss.TencentCosCredentialService;
|
||||
import com.red.circle.external.inner.service.oss.OssServiceClientService;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* oss 服务api.
|
||||
@ -16,11 +20,27 @@ import org.springframework.stereotype.Service;
|
||||
public class OssServiceClientServiceImpl implements OssServiceClientService {
|
||||
|
||||
private final OssService ossService;
|
||||
private final AliYunStsService aliYunStsService;
|
||||
private final TencentCosCredentialService tencentCosCredentialService;
|
||||
|
||||
@Override
|
||||
public Object sts() {
|
||||
return aliYunStsService.getAcsResponse();
|
||||
return tencentCosCredentialService.getResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(MultipartFile file, String key) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("file is empty");
|
||||
}
|
||||
if (StringUtils.isBlank(key)) {
|
||||
throw new IllegalArgumentException("file key is blank");
|
||||
}
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
ossService.uploadInputStream(inputStream, key);
|
||||
return ossService.getAccessUrl(key);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("upload file to object storage failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.red.circle;
|
||||
|
||||
import com.red.circle.component.oss.OssAutoConfiguration;
|
||||
import com.red.circle.component.redis.RedisAutoConfiguration;
|
||||
import com.red.circle.framework.cloud.annotation.RedCircleCloudApplication;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -18,7 +19,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@EnableScheduling
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients
|
||||
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
|
||||
@SpringBootApplication(exclude = {RedisAutoConfiguration.class, OssAutoConfiguration.class})
|
||||
public class ExternalServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -32,3 +32,12 @@ management:
|
||||
rtc:
|
||||
appId: f424387a480e41088239416e10030034
|
||||
certificate: c96d8494f80542bb83c843ad4045df03
|
||||
|
||||
red-circle:
|
||||
oss:
|
||||
tencent-cos:
|
||||
secret-id: ${TENCENT_COS_SECRET_ID:}
|
||||
secret-key: ${TENCENT_COS_SECRET_KEY:}
|
||||
bucket-name: ${TENCENT_COS_BUCKET_NAME:asset-global-interaction-1420526837}
|
||||
region: ${TENCENT_COS_REGION:me-saudi-arabia}
|
||||
access-url: ${TENCENT_COS_ACCESS_URL:https://asset.global-interaction.com}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user