wallet新增用户工资账户功能

This commit is contained in:
tianfeng 2025-11-18 14:48:22 +08:00
parent 17d212e41e
commit 63a0b87066
30 changed files with 1901 additions and 0 deletions

View File

@ -0,0 +1,62 @@
package com.red.circle.wallet.adapter.app;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.wallet.app.dto.clientobject.SalaryAccountBalanceCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryIssueResultCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryRunningWaterCO;
import com.red.circle.wallet.app.dto.cmd.SalaryIssueCmd;
import com.red.circle.wallet.app.dto.cmd.SalaryRunningWaterQueryCmd;
import com.red.circle.wallet.app.dto.cmd.SalaryWithdrawCmd;
import com.red.circle.wallet.app.service.SalaryAccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 工资账户控制器.
*/
@RestController
@RequestMapping("/salary-account")
@RequiredArgsConstructor
public class SalaryAccountRestController extends BaseController {
private final SalaryAccountService salaryAccountService;
/**
* 查询余额.
*/
@GetMapping("/balance")
public SalaryAccountBalanceCO getBalance(@RequestParam Long userId) {
return salaryAccountService.getBalance(userId);
}
/**
* 发放工资.
*/
@PostMapping("/issue")
public SalaryIssueResultCO issueSalary(@RequestBody @Validated SalaryIssueCmd cmd) {
return salaryAccountService.issueSalary(cmd);
}
/**
* 提现.
*/
@PostMapping("/withdraw")
public void withdrawSalary(@RequestBody @Validated SalaryWithdrawCmd cmd) {
salaryAccountService.withdrawSalary(cmd);
}
/**
* 查询流水.
*/
@GetMapping("/running-water")
public PageResult<SalaryRunningWaterCO> queryRunningWater(@Validated SalaryRunningWaterQueryCmd cmd) {
return salaryAccountService.queryRunningWater(cmd);
}
}

View File

@ -0,0 +1,27 @@
package com.red.circle.wallet.app.command.salary;
import com.red.circle.wallet.app.convertor.SalaryAccountConvertor;
import com.red.circle.wallet.app.dto.clientobject.SalaryIssueResultCO;
import com.red.circle.wallet.app.dto.cmd.SalaryIssueCmd;
import com.red.circle.wallet.domain.gateway.SalaryAccountGateway;
import com.red.circle.wallet.domain.salary.SalaryReceipt;
import com.red.circle.wallet.domain.salary.SalaryReceiptRes;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 发放工资.
*/
@Component
@RequiredArgsConstructor
public class SalaryIssueCmdExe {
private final SalaryAccountGateway salaryAccountGateway;
private final SalaryAccountConvertor salaryAccountConvertor;
public SalaryIssueResultCO execute(SalaryIssueCmd cmd) {
SalaryReceipt receipt = salaryAccountConvertor.toSalaryReceipt(cmd);
SalaryReceiptRes res = salaryAccountGateway.changeBalance(receipt);
return salaryAccountConvertor.toIssueResultCO(res);
}
}

View File

@ -0,0 +1,24 @@
package com.red.circle.wallet.app.command.salary;
import com.red.circle.wallet.app.convertor.SalaryAccountConvertor;
import com.red.circle.wallet.app.dto.cmd.SalaryWithdrawCmd;
import com.red.circle.wallet.domain.gateway.SalaryAccountGateway;
import com.red.circle.wallet.domain.salary.SalaryReceipt;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 工资提现.
*/
@Component
@RequiredArgsConstructor
public class SalaryWithdrawCmdExe {
private final SalaryAccountGateway salaryAccountGateway;
private final SalaryAccountConvertor salaryAccountConvertor;
public void execute(SalaryWithdrawCmd cmd) {
SalaryReceipt receipt = salaryAccountConvertor.toWithdrawReceipt(cmd);
salaryAccountGateway.changeBalance(receipt);
}
}

View File

@ -0,0 +1,41 @@
package com.red.circle.wallet.app.command.salary.query;
import com.red.circle.wallet.app.convertor.SalaryAccountConvertor;
import com.red.circle.wallet.app.dto.clientobject.SalaryAccountBalanceCO;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountBalance;
import com.red.circle.wallet.infra.database.rds.service.salary.SalaryAccountBalanceService;
import java.math.BigDecimal;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 查询工资账户余额.
*/
@Component
@RequiredArgsConstructor
public class SalaryAccountBalanceQryExe {
private final SalaryAccountBalanceService salaryAccountBalanceService;
private final SalaryAccountConvertor salaryAccountConvertor;
public SalaryAccountBalanceCO execute(Long userId) {
SalaryAccountBalance balance = salaryAccountBalanceService.lambdaQuery()
.eq(SalaryAccountBalance::getUserId, userId)
.one();
if (Objects.isNull(balance)) {
// 账户不存在返回空余额
SalaryAccountBalanceCO co = new SalaryAccountBalanceCO();
co.setUserId(userId);
co.setEarnPoints(BigDecimal.ZERO);
co.setConsumptionPoints(BigDecimal.ZERO);
co.setBalance(BigDecimal.ZERO);
co.setFrozenAmount(BigDecimal.ZERO);
co.setAvailableBalance(BigDecimal.ZERO);
return co;
}
return salaryAccountConvertor.toBalanceCO(balance);
}
}

View File

@ -0,0 +1,79 @@
package com.red.circle.wallet.app.command.salary.query;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.wallet.app.convertor.SalaryAccountConvertor;
import com.red.circle.wallet.app.dto.clientobject.SalaryRunningWaterCO;
import com.red.circle.wallet.app.dto.cmd.SalaryRunningWaterQueryCmd;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountRunningWater;
import com.red.circle.wallet.infra.database.rds.service.salary.SalaryAccountRunningWaterService;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 查询工资流水.
*/
@Component
@RequiredArgsConstructor
public class SalaryRunningWaterQryExe {
private final SalaryAccountRunningWaterService salaryAccountRunningWaterService;
private final SalaryAccountConvertor salaryAccountConvertor;
public PageResult<SalaryRunningWaterCO> execute(SalaryRunningWaterQueryCmd cmd) {
LambdaQueryWrapper<SalaryAccountRunningWater> wrapper = new LambdaQueryWrapper<>();
if (Objects.nonNull(cmd.getUserId())) {
wrapper.eq(SalaryAccountRunningWater::getUserId, cmd.getUserId());
}
if (StringUtils.isNotBlank(cmd.getSalaryType())) {
wrapper.eq(SalaryAccountRunningWater::getSalaryType, cmd.getSalaryType());
}
if (Objects.nonNull(cmd.getType())) {
wrapper.eq(SalaryAccountRunningWater::getType, cmd.getType());
}
if (Objects.nonNull(cmd.getBillBelong())) {
wrapper.eq(SalaryAccountRunningWater::getBillBelong, cmd.getBillBelong());
}
if (StringUtils.isNotBlank(cmd.getStartTime())) {
LocalDateTime startTime = LocalDateTime.parse(cmd.getStartTime(),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
wrapper.ge(SalaryAccountRunningWater::getCreateTime, startTime);
}
if (StringUtils.isNotBlank(cmd.getEndTime())) {
LocalDateTime endTime = LocalDateTime.parse(cmd.getEndTime(),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
wrapper.le(SalaryAccountRunningWater::getCreateTime, endTime);
}
wrapper.orderByDesc(SalaryAccountRunningWater::getCreateTime);
Page<SalaryAccountRunningWater> page = new Page<>(cmd.getCurrent(), cmd.getSize());
IPage<SalaryAccountRunningWater> pageResult = salaryAccountRunningWaterService.page(page, wrapper);
List<SalaryRunningWaterCO> records = pageResult.getRecords().stream()
.map(salaryAccountConvertor::toRunningWaterCO)
.collect(Collectors.toList());
PageResult<SalaryRunningWaterCO> result = new PageResult<>();
result.setRecords(records);
result.setTotal(page.getTotal());
result.setCurrent(page.getCurrent());
result.setSize(page.getSize());
return result;
}
}

View File

@ -0,0 +1,130 @@
package com.red.circle.wallet.app.convertor;
import com.red.circle.common.business.core.enums.OpUserType;
import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.tool.core.tuple.PennyAmount;
import com.red.circle.wallet.app.dto.clientobject.SalaryAccountBalanceCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryIssueResultCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryRunningWaterCO;
import com.red.circle.wallet.app.dto.cmd.SalaryIssueCmd;
import com.red.circle.wallet.app.dto.cmd.SalaryWithdrawCmd;
import com.red.circle.wallet.domain.salary.SalaryEvent;
import com.red.circle.wallet.domain.salary.SalaryReceipt;
import com.red.circle.wallet.domain.salary.SalaryReceiptRes;
import com.red.circle.wallet.domain.salary.SalaryType;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountBalance;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountRunningWater;
import java.math.BigDecimal;
import java.util.Objects;
import com.red.circle.wallet.infra.util.TimeConvertUtils;
import org.springframework.stereotype.Component;
/**
* 工资账户转换器.
*/
@Component
public class SalaryAccountConvertor {
public SalaryReceipt toSalaryReceipt(SalaryIssueCmd cmd) {
return new SalaryReceipt()
.setReceiptType(ReceiptType.INCOME)
.setUserId(cmd.getUserId())
.setSysOrigin(cmd.getReqSysOrigin().getOrigin())
.setSalaryType(SalaryType.of(cmd.getSalaryType()))
.setSalaryEvent(SalaryEvent.SALARY_ISSUE)
.setAmount(PennyAmount.ofDollar(cmd.getAmount()))
.setTrackId(cmd.getTrackId())
.setBillBelong(cmd.getBillBelong())
.setTeamId(cmd.getTeamId())
.setBdId(cmd.getBdId())
.setAgentId(cmd.getAgentId())
.setEventDesc(cmd.getEventDesc())
.setRemark(cmd.getRemark())
.setOpUserType(OpUserType.APP)
.setOpUserId(cmd.getReqUserId());
}
public SalaryReceipt toWithdrawReceipt(SalaryWithdrawCmd cmd) {
SalaryReceipt receipt = new SalaryReceipt()
.setReceiptType(ReceiptType.EXPENDITURE)
.setUserId(cmd.getUserId())
.setSysOrigin(cmd.getReqSysOrigin().getOrigin())
.setSalaryType(SalaryType.BD_SALARY)
.setSalaryEvent(SalaryEvent.WITHDRAW)
.setAmount(PennyAmount.ofDollar(cmd.getAmount()))
.setTrackId(cmd.getTrackId())
.setEventDesc("提现")
.setRemark(cmd.getRemark())
.setOpUserType(OpUserType.APP)
.setOpUserId(cmd.getReqUserId());
if (Objects.nonNull(cmd.getServiceCharge()) && cmd.getServiceCharge() > 0) {
receipt.setServiceCharge(cmd.getServiceCharge());
BigDecimal chargeRate = new BigDecimal(cmd.getServiceCharge()).divide(new BigDecimal("1000"));
BigDecimal actualAmount = cmd.getAmount().multiply(BigDecimal.ONE.subtract(chargeRate));
receipt.setActualAmount(PennyAmount.ofDollar(actualAmount));
} else {
receipt.setActualAmount(PennyAmount.ofDollar(cmd.getAmount()));
}
return receipt;
}
public SalaryIssueResultCO toIssueResultCO(SalaryReceiptRes res) {
SalaryIssueResultCO co = new SalaryIssueResultCO();
co.setRecordId(res.getRecordId());
co.setAmount(res.getAmount().getDollarAmount());
co.setBalance(res.getBalance().getDollarAmount());
co.setAvailableBalance(res.getAvailableBalance().getDollarAmount());
return co;
}
public SalaryAccountBalanceCO toBalanceCO(SalaryAccountBalance entity) {
if (Objects.isNull(entity)) {
return null;
}
SalaryAccountBalanceCO co = new SalaryAccountBalanceCO();
co.setUserId(entity.getUserId());
co.setEarnPoints(entity.getEarnPoints());
co.setConsumptionPoints(entity.getConsumptionPoints());
co.setBalance(entity.getBalance());
co.setFrozenAmount(entity.getFrozenAmount());
co.setAvailableBalance(entity.getAvailableBalance());
co.setUpdatedAt(TimeConvertUtils.toLocalDateTime(entity.getUpdateTime()));
return co;
}
public SalaryRunningWaterCO toRunningWaterCO(SalaryAccountRunningWater entity) {
if (Objects.isNull(entity)) {
return null;
}
SalaryRunningWaterCO co = new SalaryRunningWaterCO();
co.setId(entity.getId());
co.setUserId(entity.getUserId());
co.setType(entity.getType());
co.setSalaryType(entity.getSalaryType());
SalaryType salaryType = SalaryType.of(entity.getSalaryType());
if (Objects.nonNull(salaryType)) {
co.setSalaryTypeName(salaryType.getDesc());
}
co.setAmount(entity.getAmount());
co.setBalance(entity.getBalance());
co.setAvailableBalance(entity.getAvailableBalance());
co.setSalaryEvent(entity.getSalaryEvent());
co.setEventDesc(entity.getEventDesc());
co.setTrackId(entity.getTrackId());
co.setBillBelong(entity.getBillBelong());
co.setTeamId(entity.getTeamId());
co.setBdId(entity.getBdId());
co.setAgentId(entity.getAgentId());
co.setRemark(entity.getRemark());
co.setCreatedAt(TimeConvertUtils.toLocalDateTime(entity.getCreateTime()));
return co;
}
}

View File

@ -0,0 +1,48 @@
package com.red.circle.wallet.app.service;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.wallet.app.command.salary.SalaryIssueCmdExe;
import com.red.circle.wallet.app.command.salary.SalaryWithdrawCmdExe;
import com.red.circle.wallet.app.command.salary.query.SalaryAccountBalanceQryExe;
import com.red.circle.wallet.app.command.salary.query.SalaryRunningWaterQryExe;
import com.red.circle.wallet.app.dto.clientobject.SalaryAccountBalanceCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryIssueResultCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryRunningWaterCO;
import com.red.circle.wallet.app.dto.cmd.SalaryIssueCmd;
import com.red.circle.wallet.app.dto.cmd.SalaryRunningWaterQueryCmd;
import com.red.circle.wallet.app.dto.cmd.SalaryWithdrawCmd;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 工资账户服务实现.
*/
@Service
@RequiredArgsConstructor
public class SalaryAccountServiceImpl implements SalaryAccountService {
private final SalaryAccountBalanceQryExe salaryAccountBalanceQryExe;
private final SalaryRunningWaterQryExe salaryRunningWaterQryExe;
private final SalaryIssueCmdExe salaryIssueCmdExe;
private final SalaryWithdrawCmdExe salaryWithdrawCmdExe;
@Override
public SalaryAccountBalanceCO getBalance(Long userId) {
return salaryAccountBalanceQryExe.execute(userId);
}
@Override
public SalaryIssueResultCO issueSalary(SalaryIssueCmd cmd) {
return salaryIssueCmdExe.execute(cmd);
}
@Override
public void withdrawSalary(SalaryWithdrawCmd cmd) {
salaryWithdrawCmdExe.execute(cmd);
}
@Override
public PageResult<SalaryRunningWaterCO> queryRunningWater(SalaryRunningWaterQueryCmd cmd) {
return salaryRunningWaterQryExe.execute(cmd);
}
}

View File

@ -0,0 +1,47 @@
package com.red.circle.wallet.app.dto.clientobject;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 工资账户余额.
*/
@Data
public class SalaryAccountBalanceCO {
/**
* 用户ID.
*/
private Long userId;
/**
* 累计收入.
*/
private BigDecimal earnPoints;
/**
* 累计支出.
*/
private BigDecimal consumptionPoints;
/**
* 当前余额.
*/
private BigDecimal balance;
/**
* 冻结金额.
*/
private BigDecimal frozenAmount;
/**
* 可用余额.
*/
private BigDecimal availableBalance;
/**
* 更新时间.
*/
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,31 @@
package com.red.circle.wallet.app.dto.clientobject;
import java.math.BigDecimal;
import lombok.Data;
/**
* 工资发放结果.
*/
@Data
public class SalaryIssueResultCO {
/**
* 流水记录ID.
*/
private Long recordId;
/**
* 发放金额.
*/
private BigDecimal amount;
/**
* 变动后余额.
*/
private BigDecimal balance;
/**
* 变动后可用余额.
*/
private BigDecimal availableBalance;
}

View File

@ -0,0 +1,97 @@
package com.red.circle.wallet.app.dto.clientobject;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 工资流水.
*/
@Data
public class SalaryRunningWaterCO {
/**
* 流水ID.
*/
private Long id;
/**
* 用户ID.
*/
private Long userId;
/**
* 收支类型 0.收入 1.支出.
*/
private Integer type;
/**
* 工资类型.
*/
private String salaryType;
/**
* 工资类型名称.
*/
private String salaryTypeName;
/**
* 变动金额.
*/
private BigDecimal amount;
/**
* 变动后余额.
*/
private BigDecimal balance;
/**
* 变动后可用余额.
*/
private BigDecimal availableBalance;
/**
* 工资事件.
*/
private String salaryEvent;
/**
* 事件描述.
*/
private String eventDesc;
/**
* 业务单号.
*/
private String trackId;
/**
* 账单归属YYYYMM.
*/
private Integer billBelong;
/**
* 团队ID.
*/
private Long teamId;
/**
* BD ID.
*/
private Long bdId;
/**
* 代理ID.
*/
private Long agentId;
/**
* 备注.
*/
private String remark;
/**
* 创建时间.
*/
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,73 @@
package com.red.circle.wallet.app.dto.cmd;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 发放工资命令.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class SalaryIssueCmd extends AppExtCommand {
/**
* 用户ID.
*/
@NotNull(message = "userId required.")
private Long userId;
/**
* 工资类型.
*/
@NotBlank(message = "salaryType required.")
private String salaryType;
/**
* 发放金额.
*/
@NotNull(message = "amount required.")
@DecimalMin(value = "0.01", message = "amount must > 0")
private BigDecimal amount;
/**
* 账单归属YYYYMM.
*/
@NotNull(message = "billBelong required.")
private Integer billBelong;
/**
* 业务单号.
*/
@NotBlank(message = "trackId required.")
private String trackId;
/**
* 团队ID.
*/
private Long teamId;
/**
* BD ID.
*/
private Long bdId;
/**
* 代理ID.
*/
private Long agentId;
/**
* 事件描述.
*/
private String eventDesc;
/**
* 备注.
*/
private String remark;
}

View File

@ -0,0 +1,43 @@
package com.red.circle.wallet.app.dto.cmd;
import com.red.circle.common.business.dto.PageQueryCmd;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工资流水查询命令.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class SalaryRunningWaterQueryCmd extends PageQueryCmd {
/**
* 用户ID.
*/
private Long userId;
/**
* 工资类型.
*/
private String salaryType;
/**
* 收支类型 0.收入 1.支出.
*/
private Integer type;
/**
* 账单归属YYYYMM.
*/
private Integer billBelong;
/**
* 开始时间.
*/
private String startTime;
/**
* 结束时间.
*/
private String endTime;
}

View File

@ -0,0 +1,46 @@
package com.red.circle.wallet.app.dto.cmd;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 提现命令.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class SalaryWithdrawCmd extends AppExtCommand {
/**
* 用户ID.
*/
@NotNull(message = "userId required.")
private Long userId;
/**
* 提现金额.
*/
@NotNull(message = "amount required.")
@DecimalMin(value = "0.01", message = "amount must > 0")
private BigDecimal amount;
/**
* 业务单号.
*/
@NotBlank(message = "trackId required.")
private String trackId;
/**
* 手续费比例千分比.
*/
private Long serviceCharge;
/**
* 备注.
*/
private String remark;
}

View File

@ -0,0 +1,35 @@
package com.red.circle.wallet.app.service;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.wallet.app.dto.clientobject.SalaryAccountBalanceCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryIssueResultCO;
import com.red.circle.wallet.app.dto.clientobject.SalaryRunningWaterCO;
import com.red.circle.wallet.app.dto.cmd.SalaryIssueCmd;
import com.red.circle.wallet.app.dto.cmd.SalaryRunningWaterQueryCmd;
import com.red.circle.wallet.app.dto.cmd.SalaryWithdrawCmd;
/**
* 工资账户服务.
*/
public interface SalaryAccountService {
/**
* 查询余额.
*/
SalaryAccountBalanceCO getBalance(Long userId);
/**
* 发放工资.
*/
SalaryIssueResultCO issueSalary(SalaryIssueCmd cmd);
/**
* 提现.
*/
void withdrawSalary(SalaryWithdrawCmd cmd);
/**
* 查询流水.
*/
PageResult<SalaryRunningWaterCO> queryRunningWater(SalaryRunningWaterQueryCmd cmd);
}

View File

@ -0,0 +1,35 @@
package com.red.circle.wallet.domain.gateway;
import com.red.circle.tool.core.tuple.PennyAmount;
import com.red.circle.wallet.domain.salary.SalaryReceipt;
import com.red.circle.wallet.domain.salary.SalaryReceiptRes;
/**
* 工资账户 Gateway.
*/
public interface SalaryAccountGateway {
/**
* 查询余额.
*
* @param userId 用户ID
* @return 余额
*/
PennyAmount getBalance(Long userId);
/**
* 查询可用余额.
*
* @param userId 用户ID
* @return 可用余额
*/
PennyAmount getAvailableBalance(Long userId);
/**
* 变更余额.
*
* @param receipt 工资凭证
* @return 凭证响应
*/
SalaryReceiptRes changeBalance(SalaryReceipt receipt);
}

View File

@ -0,0 +1,64 @@
package com.red.circle.wallet.domain.salary;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 工资事件枚举.
*/
@Getter
@AllArgsConstructor
public enum SalaryEvent {
/**
* 工资发放.
*/
SALARY_ISSUE("SALARY_ISSUE", "工资发放"),
/**
* 工资调整.
*/
SALARY_ADJUST("SALARY_ADJUST", "工资调整"),
/**
* 提现.
*/
WITHDRAW("WITHDRAW", "提现"),
/**
* 提现退回.
*/
WITHDRAW_REFUND("WITHDRAW_REFUND", "提现退回"),
/**
* 手动增加.
*/
MANUAL_ADD("MANUAL_ADD", "手动增加"),
/**
* 手动扣减.
*/
MANUAL_DEDUCT("MANUAL_DEDUCT", "手动扣减"),
/**
* 冻结.
*/
FREEZE("FREEZE", "冻结"),
/**
* 解冻.
*/
UNFREEZE("UNFREEZE", "解冻");
private final String code;
private final String desc;
public static SalaryEvent of(String code) {
for (SalaryEvent event : values()) {
if (event.getCode().equals(code)) {
return event;
}
}
return null;
}
}

View File

@ -0,0 +1,106 @@
package com.red.circle.wallet.domain.salary;
import com.red.circle.common.business.core.enums.OpUserType;
import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.tool.core.tuple.PennyAmount;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 工资凭证.
*/
@Data
@Accessors(chain = true)
public class SalaryReceipt {
/**
* 收支类型.
*/
private ReceiptType receiptType;
/**
* 用户ID.
*/
private Long userId;
/**
* 系统来源.
*/
private String sysOrigin;
/**
* 工资类型.
*/
private SalaryType salaryType;
/**
* 工资事件.
*/
private SalaryEvent salaryEvent;
/**
* 变动金额.
*/
private PennyAmount amount;
/**
* 业务单号幂等键.
*/
private String trackId;
/**
* 账单归属YYYYMM.
*/
private Integer billBelong;
/**
* 团队ID.
*/
private Long teamId;
/**
* BD ID.
*/
private Long bdId;
/**
* 代理ID.
*/
private Long agentId;
/**
* 事件描述.
*/
private String eventDesc;
/**
* 备注.
*/
private String remark;
/**
* 操作用户类型.
*/
private OpUserType opUserType;
/**
* 操作用户ID.
*/
private Long opUserId;
/**
* 创建时间.
*/
private LocalDateTime createTime;
/**
* 手续费比例千分比.
*/
private Long serviceCharge;
/**
* 实际到账金额.
*/
private PennyAmount actualAmount;
}

View File

@ -0,0 +1,42 @@
package com.red.circle.wallet.domain.salary;
import com.red.circle.tool.core.tuple.PennyAmount;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 工资凭证响应.
*/
@Data
@Accessors(chain = true)
public class SalaryReceiptRes {
/**
* 流水记录ID.
*/
private Long recordId;
/**
* 变动后余额.
*/
private PennyAmount balance;
/**
* 变动后可用余额.
*/
private PennyAmount availableBalance;
/**
* 变动金额.
*/
private PennyAmount amount;
public static SalaryReceiptRes of(Long recordId, PennyAmount balance,
PennyAmount availableBalance, PennyAmount amount) {
return new SalaryReceiptRes()
.setRecordId(recordId)
.setBalance(balance)
.setAvailableBalance(availableBalance)
.setAmount(amount);
}
}

View File

@ -0,0 +1,51 @@
package com.red.circle.wallet.domain.salary;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 工资类型枚举.
*/
@Getter
@AllArgsConstructor
public enum SalaryType {
/**
* BD工资.
*/
BD_SALARY("BD_SALARY", "BD工资"),
/**
* 代理工资.
*/
AGENT_SALARY("AGENT_SALARY", "代理工资"),
/**
* 主播工资.
*/
ANCHOR_SALARY("ANCHOR_SALARY", "主播工资");
private final String code;
private final String desc;
public static SalaryType of(String code) {
for (SalaryType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
public boolean isBdSalary() {
return this == BD_SALARY;
}
public boolean isAgentSalary() {
return this == AGENT_SALARY;
}
public boolean isAnchorSalary() {
return this == ANCHOR_SALARY;
}
}

View File

@ -0,0 +1,35 @@
package com.red.circle.wallet.infra.database.rds.dao.salary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountBalance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 工资账户余额 Mapper.
*/
@Mapper
public interface SalaryAccountBalanceMapper extends BaseMapper<SalaryAccountBalance> {
/**
* 增加收入累计.
*/
int incrIncome(@Param("userId") Long userId, @Param("amount") Long amount);
/**
* 增加支出累计.
*/
int incrExpenditure(@Param("userId") Long userId, @Param("amount") Long amount);
/**
* 冻结金额.
*/
int freezeAmount(@Param("userId") Long userId, @Param("amount") Long amount,
@Param("version") Integer version);
/**
* 解冻金额.
*/
int unfreezeAmount(@Param("userId") Long userId, @Param("amount") Long amount,
@Param("version") Integer version);
}

View File

@ -0,0 +1,13 @@
package com.red.circle.wallet.infra.database.rds.dao.salary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountRunningWater;
import org.apache.ibatis.annotations.Mapper;
/**
* 工资账户流水 Mapper.
*/
@Mapper
public interface SalaryAccountRunningWaterMapper extends BaseMapper<SalaryAccountRunningWater> {
}

View File

@ -0,0 +1,81 @@
package com.red.circle.wallet.infra.database.rds.entity.salary;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户工资账户余额表.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("user_salary_account_balance")
public class SalaryAccountBalance extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID.
*/
@TableField("user_id")
private Long userId;
/**
* 系统来源.
*/
@TableField("sys_origin")
private String sysOrigin;
/**
* 累计收入总额.
*/
@TableField("earn_points")
private BigDecimal earnPoints;
/**
* 累计支出总额.
*/
@TableField("consumption_points")
private BigDecimal consumptionPoints;
/**
* 当前余额.
*/
@TableField("balance")
private BigDecimal balance;
/**
* 冻结金额.
*/
@TableField("frozen_amount")
private BigDecimal frozenAmount;
/**
* 可用余额.
*/
@TableField("available_balance")
private BigDecimal availableBalance;
/**
* 乐观锁版本号.
*/
@Version
@TableField("version")
private Integer version;
}

View File

@ -0,0 +1,145 @@
package com.red.circle.wallet.infra.database.rds.entity.salary;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户工资账户流水表.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("user_salary_account_running_water")
public class SalaryAccountRunningWater extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID.
*/
@TableField("user_id")
private Long userId;
/**
* 系统来源.
*/
@TableField("sys_origin")
private String sysOrigin;
/**
* 收支类型 0.收入 1.支出.
*/
@TableField("type")
private Integer type;
/**
* 工资类型.
*/
@TableField("salary_type")
private String salaryType;
/**
* 变动金额.
*/
@TableField("amount")
private BigDecimal amount;
/**
* 变动后余额.
*/
@TableField("balance")
private BigDecimal balance;
/**
* 变动后可用余额.
*/
@TableField("available_balance")
private BigDecimal availableBalance;
/**
* 手续费比例千分比.
*/
@TableField("service_charge")
private Long serviceCharge;
/**
* 实际到账金额.
*/
@TableField("actual_amount")
private BigDecimal actualAmount;
/**
* 临时余额.
*/
@TableField("temp_balance")
private BigDecimal tempBalance;
/**
* 业务单号.
*/
@TableField("track_id")
private String trackId;
/**
* 工资事件.
*/
@TableField("salary_event")
private String salaryEvent;
/**
* 事件描述.
*/
@TableField("event_desc")
private String eventDesc;
/**
* 备注.
*/
@TableField("remark")
private String remark;
/**
* 创建用户来源 0.app 1.后台.
*/
@TableField("create_user_origin")
private Integer createUserOrigin;
/**
* 账单归属YYYYMM.
*/
@TableField("bill_belong")
private Integer billBelong;
/**
* 团队ID.
*/
@TableField("team_id")
private Long teamId;
/**
* BD ID.
*/
@TableField("bd_id")
private Long bdId;
/**
* 代理ID.
*/
@TableField("agent_id")
private Long agentId;
}

View File

@ -0,0 +1,45 @@
package com.red.circle.wallet.infra.database.rds.service.salary;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountBalance;
/**
* 工资账户余额服务.
*/
public interface SalaryAccountBalanceService extends IService<SalaryAccountBalance> {
/**
* 初始化账户.
*/
void init(String sysOrigin, Long userId);
/**
* 查询余额.
*/
Long getBalance(Long userId);
/**
* 查询可用余额.
*/
Long getAvailableBalance(Long userId);
/**
* 增加收入.
*/
boolean incrIncome(Long userId, Long amount);
/**
* 增加支出.
*/
boolean incrExpenditure(Long userId, Long amount);
/**
* 冻结金额.
*/
boolean freezeAmount(Long userId, Long amount, Integer version);
/**
* 解冻金额.
*/
boolean unfreezeAmount(Long userId, Long amount, Integer version);
}

View File

@ -0,0 +1,11 @@
package com.red.circle.wallet.infra.database.rds.service.salary;
import com.baomidou.mybatisplus.extension.service.IService;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountRunningWater;
/**
* 工资账户流水服务.
*/
public interface SalaryAccountRunningWaterService extends IService<SalaryAccountRunningWater> {
}

View File

@ -0,0 +1,78 @@
package com.red.circle.wallet.infra.database.rds.service.salary.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.wallet.infra.database.rds.dao.salary.SalaryAccountBalanceMapper;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountBalance;
import com.red.circle.wallet.infra.database.rds.service.salary.SalaryAccountBalanceService;
import java.math.BigDecimal;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 工资账户余额服务实现.
*/
@Service
@RequiredArgsConstructor
public class SalaryAccountBalanceServiceImpl
extends ServiceImpl<SalaryAccountBalanceMapper, SalaryAccountBalance>
implements SalaryAccountBalanceService {
@Override
public void init(String sysOrigin, Long userId) {
SalaryAccountBalance balance = new SalaryAccountBalance()
.setId(IdWorkerUtils.getId())
.setUserId(userId)
.setSysOrigin(sysOrigin)
.setEarnPoints(BigDecimal.ZERO)
.setConsumptionPoints(BigDecimal.ZERO)
.setBalance(BigDecimal.ZERO)
.setFrozenAmount(BigDecimal.ZERO)
.setAvailableBalance(BigDecimal.ZERO)
.setVersion(0);
save(balance);
}
@Override
public Long getBalance(Long userId) {
SalaryAccountBalance balance = lambdaQuery()
.eq(SalaryAccountBalance::getUserId, userId)
.one();
if (Objects.isNull(balance)) {
return 0L;
}
return balance.getBalance().multiply(new BigDecimal("100")).longValue();
}
@Override
public Long getAvailableBalance(Long userId) {
SalaryAccountBalance balance = lambdaQuery()
.eq(SalaryAccountBalance::getUserId, userId)
.one();
if (Objects.isNull(balance)) {
return 0L;
}
return balance.getAvailableBalance().multiply(new BigDecimal("100")).longValue();
}
@Override
public boolean incrIncome(Long userId, Long amount) {
return baseMapper.incrIncome(userId, amount) > 0;
}
@Override
public boolean incrExpenditure(Long userId, Long amount) {
return baseMapper.incrExpenditure(userId, amount) > 0;
}
@Override
public boolean freezeAmount(Long userId, Long amount, Integer version) {
return baseMapper.freezeAmount(userId, amount, version) > 0;
}
@Override
public boolean unfreezeAmount(Long userId, Long amount, Integer version) {
return baseMapper.unfreezeAmount(userId, amount, version) > 0;
}
}

View File

@ -0,0 +1,17 @@
package com.red.circle.wallet.infra.database.rds.service.salary.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.red.circle.wallet.infra.database.rds.dao.salary.SalaryAccountRunningWaterMapper;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountRunningWater;
import com.red.circle.wallet.infra.database.rds.service.salary.SalaryAccountRunningWaterService;
import org.springframework.stereotype.Service;
/**
* 工资账户流水服务实现.
*/
@Service
public class SalaryAccountRunningWaterServiceImpl
extends ServiceImpl<SalaryAccountRunningWaterMapper, SalaryAccountRunningWater>
implements SalaryAccountRunningWaterService {
}

View File

@ -0,0 +1,154 @@
package com.red.circle.wallet.infra.gateway;
import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.web.util.ValidatorUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.tuple.PennyAmount;
import com.red.circle.wallet.domain.gateway.SalaryAccountGateway;
import com.red.circle.wallet.domain.salary.SalaryReceipt;
import com.red.circle.wallet.domain.salary.SalaryReceiptRes;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountBalance;
import com.red.circle.wallet.infra.database.rds.entity.salary.SalaryAccountRunningWater;
import com.red.circle.wallet.infra.database.rds.service.salary.SalaryAccountBalanceService;
import com.red.circle.wallet.infra.database.rds.service.salary.SalaryAccountRunningWaterService;
import com.red.circle.wallet.infra.util.TimeConvertUtils;
import com.red.circle.wallet.inner.error.WalletErrorCode;
import java.math.BigDecimal;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 工资账户 Gateway 实现.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SalaryAccountGatewayImpl implements SalaryAccountGateway {
private final SalaryAccountBalanceService salaryAccountBalanceService;
private final SalaryAccountRunningWaterService salaryAccountRunningWaterService;
@Override
public PennyAmount getBalance(Long userId) {
return PennyAmount.ofPenny(salaryAccountBalanceService.getBalance(userId));
}
@Override
public PennyAmount getAvailableBalance(Long userId) {
return PennyAmount.ofPenny(salaryAccountBalanceService.getAvailableBalance(userId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public SalaryReceiptRes changeBalance(SalaryReceipt receipt) {
if (!ValidatorUtils.validateFastPass(receipt)) {
throw new IllegalArgumentException("changeBalance required param error.");
}
// 检查账户是否存在不存在则初始化
SalaryAccountBalance balance = salaryAccountBalanceService.lambdaQuery()
.eq(SalaryAccountBalance::getUserId, receipt.getUserId())
.one();
if (Objects.isNull(balance)) {
salaryAccountBalanceService.init(receipt.getSysOrigin(), receipt.getUserId());
balance = salaryAccountBalanceService.lambdaQuery()
.eq(SalaryAccountBalance::getUserId, receipt.getUserId())
.one();
}
// 更新余额
boolean updateSuccess;
if (receipt.getReceiptType() == ReceiptType.INCOME) {
updateSuccess = salaryAccountBalanceService.incrIncome(
receipt.getUserId(),
receipt.getAmount().getPennyAmount()
);
} else {
updateSuccess = salaryAccountBalanceService.incrExpenditure(
receipt.getUserId(),
receipt.getAmount().getPennyAmount()
);
}
ResponseAssert.isTrue(WalletErrorCode.INSUFFICIENT_BALANCE, updateSuccess);
// 查询更新后的余额
Long newBalance = salaryAccountBalanceService.getBalance(receipt.getUserId());
Long newAvailableBalance = salaryAccountBalanceService.getAvailableBalance(receipt.getUserId());
// 插入流水记录
Long recordId = IdWorkerUtils.getId();
try {
SalaryAccountRunningWater runningWater = buildRunningWater(receipt, recordId,
newBalance, newAvailableBalance);
salaryAccountRunningWaterService.save(runningWater);
} catch (DuplicateKeyException e) {
// 幂等性trackId重复查询已有记录
SalaryAccountRunningWater existingWater = salaryAccountRunningWaterService.lambdaQuery()
.eq(SalaryAccountRunningWater::getTrackId, receipt.getTrackId())
.one();
if (Objects.nonNull(existingWater)) {
return SalaryReceiptRes.of(
existingWater.getId(),
PennyAmount.ofPenny(existingWater.getBalance()
.multiply(new BigDecimal("100")).longValue()),
PennyAmount.ofPenny(existingWater.getAvailableBalance()
.multiply(new BigDecimal("100")).longValue()),
receipt.getAmount()
);
}
throw e;
}
return SalaryReceiptRes.of(
recordId,
PennyAmount.ofPenny(newBalance),
PennyAmount.ofPenny(newAvailableBalance),
receipt.getAmount()
);
}
private SalaryAccountRunningWater buildRunningWater(SalaryReceipt receipt, Long recordId,
Long balance, Long availableBalance) {
SalaryAccountRunningWater water = new SalaryAccountRunningWater()
.setId(recordId)
.setUserId(receipt.getUserId())
.setSysOrigin(receipt.getSysOrigin())
.setType(receipt.getReceiptType().getType())
.setSalaryType(receipt.getSalaryType().getCode())
.setAmount(receipt.getAmount().getDollarAmount())
.setBalance(PennyAmount.ofPenny(balance).getDollarAmount())
.setAvailableBalance(PennyAmount.ofPenny(availableBalance).getDollarAmount())
.setTrackId(receipt.getTrackId())
.setSalaryEvent(receipt.getSalaryEvent().getCode())
.setEventDesc(receipt.getEventDesc())
.setRemark(receipt.getRemark())
.setCreateUserOrigin(receipt.getOpUserType().getType())
.setBillBelong(receipt.getBillBelong())
.setTeamId(receipt.getTeamId())
.setBdId(receipt.getBdId())
.setAgentId(receipt.getAgentId());
if (Objects.nonNull(receipt.getServiceCharge())) {
water.setServiceCharge(receipt.getServiceCharge());
}
if (Objects.nonNull(receipt.getActualAmount())) {
water.setActualAmount(receipt.getActualAmount().getDollarAmount());
}
water.setCreateTime(Objects.isNull(receipt.getCreateTime())
? TimestampUtils.now() : TimeConvertUtils.toTimestamp(receipt.getCreateTime()));
water.setUpdateTime(TimestampUtils.now());
water.setCreateUser(receipt.getOpUserId());
water.setUpdateUser(receipt.getOpUserId());
return water;
}
}

View File

@ -0,0 +1,199 @@
package com.red.circle.wallet.infra.util;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Objects;
/**
* 时间转换工具类.
* <p>用于 Timestamp LocalDateTime 之间的相互转换</p>
*/
public class TimeConvertUtils {
/**
* 默认时区 - UTC.
*/
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
/**
* LocalDateTime Timestamp.
*
* @param localDateTime LocalDateTime
* @return Timestamp
*/
public static Timestamp toTimestamp(LocalDateTime localDateTime) {
if (Objects.isNull(localDateTime)) {
return null;
}
return Timestamp.valueOf(localDateTime);
}
/**
* LocalDateTime Timestamp指定时区.
*
* @param localDateTime LocalDateTime
* @param zoneId 时区
* @return Timestamp
*/
public static Timestamp toTimestamp(LocalDateTime localDateTime, ZoneId zoneId) {
if (Objects.isNull(localDateTime)) {
return null;
}
Instant instant = localDateTime.atZone(zoneId).toInstant();
return Timestamp.from(instant);
}
/**
* LocalDateTime TimestampUTC时区.
*
* @param localDateTime LocalDateTime
* @return Timestamp
*/
public static Timestamp toTimestampUtc(LocalDateTime localDateTime) {
if (Objects.isNull(localDateTime)) {
return null;
}
return Timestamp.from(localDateTime.toInstant(ZoneOffset.UTC));
}
/**
* Timestamp LocalDateTime.
*
* @param timestamp Timestamp
* @return LocalDateTime
*/
public static LocalDateTime toLocalDateTime(Timestamp timestamp) {
if (Objects.isNull(timestamp)) {
return null;
}
return timestamp.toLocalDateTime();
}
/**
* Timestamp LocalDateTime指定时区.
*
* @param timestamp Timestamp
* @param zoneId 时区
* @return LocalDateTime
*/
public static LocalDateTime toLocalDateTime(Timestamp timestamp, ZoneId zoneId) {
if (Objects.isNull(timestamp)) {
return null;
}
return LocalDateTime.ofInstant(timestamp.toInstant(), zoneId);
}
/**
* Timestamp LocalDateTime系统默认时区.
*
* @param timestamp Timestamp
* @return LocalDateTime
*/
public static LocalDateTime toLocalDateTimeDefault(Timestamp timestamp) {
if (Objects.isNull(timestamp)) {
return null;
}
return LocalDateTime.ofInstant(timestamp.toInstant(), DEFAULT_ZONE_ID);
}
/**
* 毫秒时间戳 LocalDateTime.
*
* @param epochMilli 毫秒时间戳
* @return LocalDateTime
*/
public static LocalDateTime toLocalDateTime(Long epochMilli) {
if (Objects.isNull(epochMilli)) {
return null;
}
return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), DEFAULT_ZONE_ID);
}
/**
* 毫秒时间戳 LocalDateTime指定时区.
*
* @param epochMilli 毫秒时间戳
* @param zoneId 时区
* @return LocalDateTime
*/
public static LocalDateTime toLocalDateTime(Long epochMilli, ZoneId zoneId) {
if (Objects.isNull(epochMilli)) {
return null;
}
return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), zoneId);
}
/**
* LocalDateTime 毫秒时间戳.
*
* @param localDateTime LocalDateTime
* @return 毫秒时间戳
*/
public static Long toEpochMilli(LocalDateTime localDateTime) {
if (Objects.isNull(localDateTime)) {
return null;
}
return localDateTime.atZone(DEFAULT_ZONE_ID).toInstant().toEpochMilli();
}
/**
* LocalDateTime 毫秒时间戳指定时区.
*
* @param localDateTime LocalDateTime
* @param zoneId 时区
* @return 毫秒时间戳
*/
public static Long toEpochMilli(LocalDateTime localDateTime, ZoneId zoneId) {
if (Objects.isNull(localDateTime)) {
return null;
}
return localDateTime.atZone(zoneId).toInstant().toEpochMilli();
}
/**
* 秒时间戳 LocalDateTime.
*
* @param epochSecond 秒时间戳
* @return LocalDateTime
*/
public static LocalDateTime toLocalDateTimeFromSecond(Long epochSecond) {
if (Objects.isNull(epochSecond)) {
return null;
}
return LocalDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), DEFAULT_ZONE_ID);
}
/**
* LocalDateTime 秒时间戳.
*
* @param localDateTime LocalDateTime
* @return 秒时间戳
*/
public static Long toEpochSecond(LocalDateTime localDateTime) {
if (Objects.isNull(localDateTime)) {
return null;
}
return localDateTime.atZone(DEFAULT_ZONE_ID).toInstant().getEpochSecond();
}
/**
* 获取当前 LocalDateTime.
*
* @return 当前 LocalDateTime
*/
public static LocalDateTime now() {
return LocalDateTime.now(DEFAULT_ZONE_ID);
}
/**
* 获取当前 Timestamp.
*
* @return 当前 Timestamp
*/
public static Timestamp nowTimestamp() {
return new Timestamp(System.currentTimeMillis());
}
}

View File

@ -0,0 +1,42 @@
<?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.wallet.infra.database.rds.dao.salary.SalaryAccountBalanceMapper">
<update id="incrIncome">
UPDATE user_salary_account_balance
SET earn_points = earn_points + #{amount} / 100,
balance = balance + #{amount} / 100,
available_balance = available_balance + #{amount} / 100
WHERE user_id = #{userId}
</update>
<update id="incrExpenditure">
UPDATE user_salary_account_balance
SET consumption_points = consumption_points + #{amount} / 100,
balance = balance - #{amount} / 100,
available_balance = available_balance - #{amount} / 100
WHERE user_id = #{userId}
AND available_balance >= #{amount} / 100
</update>
<update id="freezeAmount">
UPDATE user_salary_account_balance
SET frozen_amount = frozen_amount + #{amount} / 100,
available_balance = available_balance - #{amount} / 100,
version = version + 1
WHERE user_id = #{userId}
AND version = #{version}
AND available_balance >= #{amount} / 100
</update>
<update id="unfreezeAmount">
UPDATE user_salary_account_balance
SET frozen_amount = frozen_amount - #{amount} / 100,
available_balance = available_balance + #{amount} / 100,
version = version + 1
WHERE user_id = #{userId}
AND version = #{version}
AND frozen_amount >= #{amount} / 100
</update>
</mapper>