优化
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.masterdata.exchange.dto.MonthlyExchangeRateDtos;
|
||||
import com.baiyue.lastleg.masterdata.exchange.service.MonthlyExchangeRateService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/master-data/exchange-rates")
|
||||
@Tag(name = "Monthly exchange rates / 月度汇率")
|
||||
public class PlatformMonthlyExchangeRateController {
|
||||
@Resource private MonthlyExchangeRateService service;
|
||||
|
||||
@GetMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.EXCHANGE_RATE_LIST + "')")
|
||||
@Operation(summary = "Page monthly exchange rates", description = "Required authority: platform:overseas-master-data:exchange-rate:list")
|
||||
public ApiResponse<PageResult<MonthlyExchangeRateDtos.Response>> page(@Valid MonthlyExchangeRateDtos.Query query) {
|
||||
return ApiResponse.success(service.page(query));
|
||||
}
|
||||
@GetMapping("/summary") @PreAuthorize("hasAuthority('" + OverseasPermissions.EXCHANGE_RATE_LIST + "')")
|
||||
public ApiResponse<MonthlyExchangeRateDtos.Summary> summary() { return ApiResponse.success(service.summary()); }
|
||||
@GetMapping("/{month}") @PreAuthorize("hasAuthority('" + OverseasPermissions.EXCHANGE_RATE_LIST + "')")
|
||||
public ApiResponse<MonthlyExchangeRateDtos.Response> detail(@PathVariable String month) { return ApiResponse.success(service.detail(month)); }
|
||||
@PostMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.EXCHANGE_RATE_CREATE + "')")
|
||||
public ApiResponse<MonthlyExchangeRateDtos.Response> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody MonthlyExchangeRateDtos.Create request) { return ApiResponse.success(service.create(operator(user), request)); }
|
||||
@PutMapping("/{month}") @PreAuthorize("hasAuthority('" + OverseasPermissions.EXCHANGE_RATE_EDIT + "')")
|
||||
public ApiResponse<MonthlyExchangeRateDtos.Response> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String month, @Valid @RequestBody MonthlyExchangeRateDtos.Update request) {
|
||||
return ApiResponse.success(service.update(month, operator(user), request));
|
||||
}
|
||||
@PatchMapping("/{month}/status") @PreAuthorize("hasAuthority('" + OverseasPermissions.EXCHANGE_RATE_STATUS + "')")
|
||||
public ApiResponse<MonthlyExchangeRateDtos.Response> status(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String month, @Valid @RequestBody MonthlyExchangeRateDtos.ChangeStatus request) {
|
||||
return ApiResponse.success(service.changeStatus(month, operator(user), request));
|
||||
}
|
||||
@GetMapping(value = "/export", produces = "text/csv")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCHANGE_RATE_EXPORT + "')")
|
||||
public ResponseEntity<byte[]> export(@Valid MonthlyExchangeRateDtos.Query query) {
|
||||
return ResponseEntity.ok().contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=monthly-exchange-rates.csv")
|
||||
.body(service.exportCsv(query));
|
||||
}
|
||||
private String operator(AuthenticatedUser user) { return user == null ? "UNKNOWN" : user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public final class MonthlyExchangeRateDtos {
|
||||
private MonthlyExchangeRateDtos() { }
|
||||
|
||||
public record Query(@Min(1) Integer page, @Min(1) @Max(200) Integer size,
|
||||
@Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])") String monthFrom,
|
||||
@Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])") String monthTo,
|
||||
String status) {
|
||||
public Query { page = page == null ? 1 : page; size = size == null ? 20 : size; }
|
||||
}
|
||||
public record Create(@NotBlank @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])") String month,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal usdToCnyAverage,
|
||||
@Size(max = 1000) String remark) { }
|
||||
public record Update(@NotNull @Min(0) Long recordVersion,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal usdToCnyAverage,
|
||||
@Size(max = 1000) String remark) { }
|
||||
public record ChangeStatus(@NotBlank String status, @NotNull @Min(0) Long recordVersion,
|
||||
@Size(max = 500) String remark) { }
|
||||
public record Response(String month, String baseCurrency, String quoteCurrency,
|
||||
BigDecimal usdToCnyAverage, String status, long recordVersion,
|
||||
String updatedBy, LocalDateTime updatedAt, String remark) { }
|
||||
public record Summary(String latestMonth, BigDecimal latestUsdToCnyAverage,
|
||||
String updatedBy, LocalDateTime updatedAt) { }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_monthly_exchange_rate")
|
||||
public class MonthlyExchangeRate {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String rateMonth;
|
||||
private String baseCurrency;
|
||||
private String quoteCurrency;
|
||||
private BigDecimal usdToCnyAverage;
|
||||
private String status;
|
||||
private String remark;
|
||||
private Long recordVersion;
|
||||
private String createdBy;
|
||||
private String updatedBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_monthly_exchange_rate_history")
|
||||
public class MonthlyExchangeRateHistory {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long exchangeRateId;
|
||||
private BigDecimal beforeRate;
|
||||
private BigDecimal afterRate;
|
||||
private String beforeStatus;
|
||||
private String afterStatus;
|
||||
private String operatorId;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.masterdata.exchange.entity.MonthlyExchangeRateHistory;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MonthlyExchangeRateHistoryMapper extends BaseMapper<MonthlyExchangeRateHistory> { }
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.masterdata.exchange.entity.MonthlyExchangeRate;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface MonthlyExchangeRateMapper extends BaseMapper<MonthlyExchangeRate> {
|
||||
@Select("SELECT * FROM ll_monthly_exchange_rate WHERE rate_month=#{month} LIMIT 1")
|
||||
MonthlyExchangeRate findMonth(@Param("month") String month);
|
||||
|
||||
@Select("SELECT * FROM ll_monthly_exchange_rate WHERE rate_month=#{month} LIMIT 1 FOR UPDATE")
|
||||
MonthlyExchangeRate lockMonth(@Param("month") String month);
|
||||
|
||||
@Select("SELECT * FROM ll_monthly_exchange_rate WHERE status='ACTIVE' ORDER BY rate_month DESC LIMIT 1")
|
||||
MonthlyExchangeRate latestActive();
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.masterdata.exchange.dto.MonthlyExchangeRateDtos;
|
||||
import com.baiyue.lastleg.masterdata.exchange.entity.MonthlyExchangeRate;
|
||||
import com.baiyue.lastleg.masterdata.exchange.entity.MonthlyExchangeRateHistory;
|
||||
import com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateHistoryMapper;
|
||||
import com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Locale;
|
||||
|
||||
@Service
|
||||
public class MonthlyExchangeRateService {
|
||||
private static final String ACTIVE = "ACTIVE";
|
||||
private static final String INACTIVE = "INACTIVE";
|
||||
@Resource private MonthlyExchangeRateMapper rates;
|
||||
@Resource private MonthlyExchangeRateHistoryMapper histories;
|
||||
|
||||
public PageResult<MonthlyExchangeRateDtos.Response> page(MonthlyExchangeRateDtos.Query query) {
|
||||
validateRange(query.monthFrom(), query.monthTo());
|
||||
String status = optionalStatus(query.status());
|
||||
Page<MonthlyExchangeRate> page = rates.selectPage(Page.of(query.page(), query.size()),
|
||||
new LambdaQueryWrapper<MonthlyExchangeRate>()
|
||||
.ge(StringUtils.hasText(query.monthFrom()), MonthlyExchangeRate::getRateMonth, query.monthFrom())
|
||||
.le(StringUtils.hasText(query.monthTo()), MonthlyExchangeRate::getRateMonth, query.monthTo())
|
||||
.eq(status != null, MonthlyExchangeRate::getStatus, status)
|
||||
.orderByDesc(MonthlyExchangeRate::getRateMonth));
|
||||
return new PageResult<>(page.getRecords().stream().map(this::response).toList(), page.getCurrent(),
|
||||
page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
public MonthlyExchangeRateDtos.Response detail(String month) { return response(required(month)); }
|
||||
|
||||
public MonthlyExchangeRateDtos.Summary summary() {
|
||||
MonthlyExchangeRate value = rates.latestActive();
|
||||
return value == null ? new MonthlyExchangeRateDtos.Summary(null, null, null, null)
|
||||
: new MonthlyExchangeRateDtos.Summary(value.getRateMonth(), value.getUsdToCnyAverage(),
|
||||
value.getUpdatedBy(), value.getUpdatedAt());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public MonthlyExchangeRateDtos.Response create(String operator, MonthlyExchangeRateDtos.Create request) {
|
||||
String month = month(request.month()); LocalDateTime now = LocalDateTime.now();
|
||||
MonthlyExchangeRate value = new MonthlyExchangeRate(); value.setId(IdWorker.getId());
|
||||
value.setRateMonth(month); value.setBaseCurrency("USD"); value.setQuoteCurrency("CNY");
|
||||
value.setUsdToCnyAverage(request.usdToCnyAverage()); value.setStatus(ACTIVE);
|
||||
value.setRemark(trim(request.remark())); value.setRecordVersion(0L);
|
||||
value.setCreatedBy(operator); value.setUpdatedBy(operator); value.setCreatedAt(now); value.setUpdatedAt(now);
|
||||
try { rates.insert(value); }
|
||||
catch (DuplicateKeyException exception) { throw conflict("该月份汇率已存在"); }
|
||||
history(value.getId(), null, value.getUsdToCnyAverage(), null, ACTIVE, operator, request.remark());
|
||||
return detail(month);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public MonthlyExchangeRateDtos.Response update(String month, String operator,
|
||||
MonthlyExchangeRateDtos.Update request) {
|
||||
MonthlyExchangeRate current = locked(month); requireVersion(current, request.recordVersion());
|
||||
int updated = rates.update(null, new LambdaUpdateWrapper<MonthlyExchangeRate>()
|
||||
.eq(MonthlyExchangeRate::getId, current.getId())
|
||||
.eq(MonthlyExchangeRate::getRecordVersion, request.recordVersion())
|
||||
.set(MonthlyExchangeRate::getUsdToCnyAverage, request.usdToCnyAverage())
|
||||
.set(MonthlyExchangeRate::getRemark, trim(request.remark()))
|
||||
.set(MonthlyExchangeRate::getUpdatedBy, operator).set(MonthlyExchangeRate::getUpdatedAt, LocalDateTime.now())
|
||||
.setSql("record_version = record_version + 1"));
|
||||
if (updated != 1) throw conflict("汇率已被其他操作修改,请刷新后重试");
|
||||
history(current.getId(), current.getUsdToCnyAverage(), request.usdToCnyAverage(),
|
||||
current.getStatus(), current.getStatus(), operator, request.remark());
|
||||
return detail(current.getRateMonth());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public MonthlyExchangeRateDtos.Response changeStatus(String month, String operator,
|
||||
MonthlyExchangeRateDtos.ChangeStatus request) {
|
||||
MonthlyExchangeRate current = locked(month); requireVersion(current, request.recordVersion());
|
||||
String status = status(request.status());
|
||||
if (status.equals(current.getStatus())) return response(current);
|
||||
int updated = rates.update(null, new LambdaUpdateWrapper<MonthlyExchangeRate>()
|
||||
.eq(MonthlyExchangeRate::getId, current.getId())
|
||||
.eq(MonthlyExchangeRate::getRecordVersion, request.recordVersion())
|
||||
.set(MonthlyExchangeRate::getStatus, status).set(MonthlyExchangeRate::getUpdatedBy, operator)
|
||||
.set(MonthlyExchangeRate::getUpdatedAt, LocalDateTime.now()).setSql("record_version = record_version + 1"));
|
||||
if (updated != 1) throw conflict("汇率已被其他操作修改,请刷新后重试");
|
||||
history(current.getId(), current.getUsdToCnyAverage(), current.getUsdToCnyAverage(),
|
||||
current.getStatus(), status, operator, request.remark());
|
||||
return detail(current.getRateMonth());
|
||||
}
|
||||
|
||||
public byte[] exportCsv(MonthlyExchangeRateDtos.Query query) {
|
||||
validateRange(query.monthFrom(), query.monthTo());
|
||||
String status = optionalStatus(query.status());
|
||||
var rows = rates.selectList(new LambdaQueryWrapper<MonthlyExchangeRate>()
|
||||
.ge(StringUtils.hasText(query.monthFrom()), MonthlyExchangeRate::getRateMonth, query.monthFrom())
|
||||
.le(StringUtils.hasText(query.monthTo()), MonthlyExchangeRate::getRateMonth, query.monthTo())
|
||||
.eq(status != null, MonthlyExchangeRate::getStatus, status)
|
||||
.orderByDesc(MonthlyExchangeRate::getRateMonth)).stream().map(this::response).toList();
|
||||
StringBuilder csv = new StringBuilder("\uFEFF月份,基准币种,报价币种,月平均汇率,状态,更新人,更新时间\r\n");
|
||||
for (var row : rows) csv.append(row.month()).append(',').append(row.baseCurrency()).append(',')
|
||||
.append(row.quoteCurrency()).append(',').append(row.usdToCnyAverage()).append(',')
|
||||
.append(row.status()).append(',').append(cell(row.updatedBy())).append(',').append(row.updatedAt()).append("\r\n");
|
||||
return csv.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void history(long id, java.math.BigDecimal beforeRate, java.math.BigDecimal afterRate,
|
||||
String beforeStatus, String afterStatus, String operator, String remark) {
|
||||
MonthlyExchangeRateHistory value = new MonthlyExchangeRateHistory(); value.setId(IdWorker.getId());
|
||||
value.setExchangeRateId(id); value.setBeforeRate(beforeRate); value.setAfterRate(afterRate);
|
||||
value.setBeforeStatus(beforeStatus); value.setAfterStatus(afterStatus); value.setOperatorId(operator);
|
||||
value.setRemark(trim(remark)); value.setCreatedAt(LocalDateTime.now()); histories.insert(value);
|
||||
}
|
||||
private MonthlyExchangeRate required(String month) {
|
||||
MonthlyExchangeRate value = rates.findMonth(month(month));
|
||||
if (value == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "月度汇率不存在"); return value;
|
||||
}
|
||||
private MonthlyExchangeRate locked(String month) {
|
||||
MonthlyExchangeRate value = rates.lockMonth(month(month));
|
||||
if (value == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "月度汇率不存在"); return value;
|
||||
}
|
||||
private MonthlyExchangeRateDtos.Response response(MonthlyExchangeRate value) {
|
||||
return new MonthlyExchangeRateDtos.Response(value.getRateMonth(), value.getBaseCurrency(),
|
||||
value.getQuoteCurrency(), value.getUsdToCnyAverage(), value.getStatus(), version(value.getRecordVersion()),
|
||||
value.getUpdatedBy(), value.getUpdatedAt(), value.getRemark());
|
||||
}
|
||||
private void requireVersion(MonthlyExchangeRate value, long expected) {
|
||||
if (version(value.getRecordVersion()) != expected) throw conflict("汇率已被其他操作修改,请刷新后重试");
|
||||
}
|
||||
private void validateRange(String from, String to) {
|
||||
if (StringUtils.hasText(from) && StringUtils.hasText(to) && from.compareTo(to) > 0) throw badRequest("monthFrom不能晚于monthTo");
|
||||
}
|
||||
private String month(String value) {
|
||||
if (value == null || !value.matches("\\d{4}-(0[1-9]|1[0-2])")) throw badRequest("月份格式必须为yyyy-MM"); return value;
|
||||
}
|
||||
private String optionalStatus(String value) { return StringUtils.hasText(value) ? status(value) : null; }
|
||||
private String status(String value) {
|
||||
String result = value.trim().toUpperCase(Locale.ROOT);
|
||||
if (!ACTIVE.equals(result) && !INACTIVE.equals(result)) throw badRequest("汇率状态只支持ACTIVE/INACTIVE"); return result;
|
||||
}
|
||||
private static long version(Long value) { return value == null ? 0L : value; }
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String cell(Object value) { return "\"" + String.valueOf(value == null ? "" : value).replace("\"", "\"\"") + "\""; }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
Reference in New Issue
Block a user