From 994d71a10cadc42e384dc7239f9df43ba0165571 Mon Sep 17 00:00:00 2001 From: ziin Date: Thu, 26 Feb 2026 21:53:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(pin):=20=E9=87=8D=E6=9E=84=E7=BD=AE?= =?UTF-8?q?=E9=A1=B6=E9=80=BB=E8=BE=91=E5=B9=B6=E6=8A=BD=E7=A6=BBPkPinServ?= =?UTF-8?q?ice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将UserController中置顶/取消置顶逻辑下沉到PkPinService,统一事务与异常处理 - UserDao新增原子增减积分方法,避免并发扣减问题 - VVTools抽取SECONDS_PER_HOUR常量并修复向上取整计算 - 新增EpochSecondProvider等接口与实现,为后续测试提供时钟桩 - 补充PkPinServiceImplTests单元测试,覆盖置顶成功、积分不足、重复取消等场景 --- .../Tools/EpochSecondProvider.java | 6 + .../Tools/SystemEpochSecondProvider.java | 12 ++ .../java/vvpkassistant/Tools/VVTools.java | 6 +- .../vvpkassistant/User/mapper/UserDao.java | 15 ++ .../config/FunctionConfigProvider.java | 6 + .../HolderBackedFunctionConfigProvider.java | 12 ++ .../controller/UserController.java | 83 ++------- .../pk/service/PkPinService.java | 8 + .../pk/service/PkPinServiceImpl.java | 167 ++++++++++++++++++ .../pk/service/PkPinServiceImplTests.java | 109 ++++++++++++ 10 files changed, 353 insertions(+), 71 deletions(-) create mode 100644 src/main/java/vvpkassistant/Tools/EpochSecondProvider.java create mode 100644 src/main/java/vvpkassistant/Tools/SystemEpochSecondProvider.java create mode 100644 src/main/java/vvpkassistant/config/FunctionConfigProvider.java create mode 100644 src/main/java/vvpkassistant/config/HolderBackedFunctionConfigProvider.java create mode 100644 src/main/java/vvpkassistant/pk/service/PkPinService.java create mode 100644 src/main/java/vvpkassistant/pk/service/PkPinServiceImpl.java create mode 100644 src/test/java/vvpkassistant/pk/service/PkPinServiceImplTests.java diff --git a/src/main/java/vvpkassistant/Tools/EpochSecondProvider.java b/src/main/java/vvpkassistant/Tools/EpochSecondProvider.java new file mode 100644 index 0000000..591de2f --- /dev/null +++ b/src/main/java/vvpkassistant/Tools/EpochSecondProvider.java @@ -0,0 +1,6 @@ +package vvpkassistant.Tools; + +public interface EpochSecondProvider { + long nowEpochSecond(); +} + diff --git a/src/main/java/vvpkassistant/Tools/SystemEpochSecondProvider.java b/src/main/java/vvpkassistant/Tools/SystemEpochSecondProvider.java new file mode 100644 index 0000000..5bcc1a4 --- /dev/null +++ b/src/main/java/vvpkassistant/Tools/SystemEpochSecondProvider.java @@ -0,0 +1,12 @@ +package vvpkassistant.Tools; + +import org.springframework.stereotype.Component; + +@Component +public class SystemEpochSecondProvider implements EpochSecondProvider { + @Override + public long nowEpochSecond() { + return VVTools.currentTimeStamp(); + } +} + diff --git a/src/main/java/vvpkassistant/Tools/VVTools.java b/src/main/java/vvpkassistant/Tools/VVTools.java index db45ec7..0d60392 100644 --- a/src/main/java/vvpkassistant/Tools/VVTools.java +++ b/src/main/java/vvpkassistant/Tools/VVTools.java @@ -17,6 +17,8 @@ import java.util.Map; ************************/ public class VVTools { + private static final long SECONDS_PER_HOUR = 3600L; + // 获取当前时间戳 public static long currentTimeStamp() { long timeStamp = Calendar.getInstance().getTimeInMillis() / 1000; @@ -117,12 +119,12 @@ public class VVTools { public static long calculateHoursRound(long expireTime, long currentTime) { if (expireTime <= currentTime) return 0; long diffSeconds = expireTime - currentTime; - return diffSeconds / 3600; + return (diffSeconds + SECONDS_PER_HOUR - 1) / SECONDS_PER_HOUR; } // 返还积分用(不足1小时忽略) public static long calculateHoursFloor(long expireTime, long currentTime) { if (expireTime <= currentTime) return 0; - return (expireTime - currentTime) / 3600; + return (expireTime - currentTime) / SECONDS_PER_HOUR; } } diff --git a/src/main/java/vvpkassistant/User/mapper/UserDao.java b/src/main/java/vvpkassistant/User/mapper/UserDao.java index e166189..91fc8cb 100644 --- a/src/main/java/vvpkassistant/User/mapper/UserDao.java +++ b/src/main/java/vvpkassistant/User/mapper/UserDao.java @@ -8,6 +8,21 @@ import vvpkassistant.User.model.UserModel; @Mapper public interface UserDao extends BaseMapper { + // 原子扣减积分:当 points >= cost 时扣减,返回受影响行数(1=成功,0=积分不足/用户不存在) + default int decreasePointsIfEnough(Integer userId, int cost) { + return update(null, Wrappers.lambdaUpdate() + .eq(UserModel::getId, userId) + .ge(UserModel::getPoints, cost) + .setSql("points = points - " + cost)); + } + + // 原子增加积分:返回受影响行数(1=成功,0=用户不存在) + default int increasePoints(Integer userId, int amount) { + return update(null, Wrappers.lambdaUpdate() + .eq(UserModel::getId, userId) + .setSql("points = points + " + amount)); + } + // 根据用户的手机号查询用户 default UserModel queryWithPhoneNumber(String phoneNumber) { return selectOne(Wrappers.lambdaQuery() diff --git a/src/main/java/vvpkassistant/config/FunctionConfigProvider.java b/src/main/java/vvpkassistant/config/FunctionConfigProvider.java new file mode 100644 index 0000000..c92b123 --- /dev/null +++ b/src/main/java/vvpkassistant/config/FunctionConfigProvider.java @@ -0,0 +1,6 @@ +package vvpkassistant.config; + +public interface FunctionConfigProvider { + String getValue(String functionName); +} + diff --git a/src/main/java/vvpkassistant/config/HolderBackedFunctionConfigProvider.java b/src/main/java/vvpkassistant/config/HolderBackedFunctionConfigProvider.java new file mode 100644 index 0000000..3916282 --- /dev/null +++ b/src/main/java/vvpkassistant/config/HolderBackedFunctionConfigProvider.java @@ -0,0 +1,12 @@ +package vvpkassistant.config; + +import org.springframework.stereotype.Component; + +@Component +public class HolderBackedFunctionConfigProvider implements FunctionConfigProvider { + @Override + public String getValue(String functionName) { + return FunctionConfigHolder.getValue(functionName); + } +} + diff --git a/src/main/java/vvpkassistant/controller/UserController.java b/src/main/java/vvpkassistant/controller/UserController.java index bac629d..3a843fb 100644 --- a/src/main/java/vvpkassistant/controller/UserController.java +++ b/src/main/java/vvpkassistant/controller/UserController.java @@ -6,7 +6,6 @@ import org.springframework.web.bind.annotation.*; import vvpkassistant.CoinRecords.CoinRecords; import vvpkassistant.CoinRecords.CoinRecordsDao; import vvpkassistant.Data.ResponseData; -import vvpkassistant.Data.ResponseInfo; import vvpkassistant.Data.WxChatParam; import vvpkassistant.User.mapper.UserDao; import vvpkassistant.User.mapper.SignInRecordDao; @@ -35,6 +34,7 @@ import vvpkassistant.pk.mapper.PkRecordDao; import vvpkassistant.pk.model.PkInfoModel; import vvpkassistant.pk.model.PkRecordDetail; import vvpkassistant.pk.mapper.PkRecordDetailDao; +import vvpkassistant.pk.service.PkPinService; import javax.annotation.Resource; import java.util.HashMap; @@ -76,6 +76,9 @@ public class UserController { @Resource private MailService mailService; + @Resource + private PkPinService pkPinService; + // 配置用户信息 @PostMapping("inputUserInfo") public ResponseData inputUserInfo(@RequestBody UserInputUserInfoDTO param) { @@ -305,81 +308,23 @@ public class UserController { // 置顶文章 @PostMapping("pinToTop") public ResponseData pinToTop(@RequestBody UserPinToTopDTO request) { - // 文章id - Integer articleId = request.getArticleId(); - PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId); - Integer userId = pkInfoModel.getSenderId(); - // 到期时间戳 - Integer pinExpireTime = request.getPinExpireTime(); - - long currentTimeStamp = VVTools.currentTimeStamp(); - long hour = VVTools.calculateHoursRound(pinExpireTime, currentTimeStamp); - String coin = FunctionConfigHolder.getValue("置顶扣除积分"); - int totalCoin = (int) (Integer.parseInt(coin) * hour); - - UserModel userModel = userDao.selectById(userId); - if (userModel != null) { - // 扣除积分 更新数据 - Integer points = userModel.getPoints(); - if (points - totalCoin > 0) { - userModel.setPoints(userModel.getPoints() - totalCoin); - userDao.updateById(userModel); - // 设置置顶到期时间 - pkInfoModel.setPinExpireTime(pinExpireTime); - // 设置创建置顶的时间 - pkInfoModel.setPinCreateTime((int) VVTools.currentTimeStamp()); - // 更新pk文章数据 - int i = pkInfoDao.updateById(pkInfoModel); - if (i == 1) { - String info = String.format("置顶成功,扣除%d积分",totalCoin); - // 增加积分变动记录 - CoinRecords coinRecords = new CoinRecords("置顶扣除积分",userId,totalCoin, (int) VVTools.currentTimeStamp(),0); - coinRecordsDao.insert(coinRecords); - // 返回给前端数据 - return ResponseData.success(info); - }else { - throw new BusinessException(ErrorCode.SYSTEM_ERROR); - } - }else { - throw new BusinessException(ErrorCode.SYSTEM_ERROR,String.format("积分不足,需要%d积分",totalCoin)); - } - }else { - throw new BusinessException(ErrorCode.SYSTEM_ERROR,"用户不存在"); + if (request == null || request.getArticleId() == null || request.getPinExpireTime() == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空"); } + int operatorUserId = Integer.parseInt(StpUtil.getLoginId().toString()); + String info = pkPinService.pinToTop(operatorUserId, request.getArticleId(), request.getPinExpireTime()); + return ResponseData.success(info); } // 取消置顶 @PostMapping("cancelPin") public ResponseData cancelPin(@RequestBody UserCancelPinDTO request) { - Integer articleId = request.getArticleId(); - PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId); - Integer pinExpireTime = pkInfoModel.getPinExpireTime(); - long hour = VVTools.calculateHoursFloor(pinExpireTime, VVTools.currentTimeStamp()); - - String coin = FunctionConfigHolder.getValue("置顶扣除积分"); - // 计算总积分。用于返还给用户 - int totalCoin = (int) (Integer.parseInt(coin) * hour); - - // 获取用户对象 - UserModel userModel = userDao.selectById(pkInfoModel.getSenderId()); - Integer points = userModel.getPoints(); - // 返还用户积分 - userModel.setPoints(points + totalCoin); - // 更新数据库 - userDao.updateById(userModel); - - // 重置置顶时间 - pkInfoModel.setPinExpireTime(0); - pkInfoModel.setPinCreateTime(0); - int i = pkInfoDao.updateById(pkInfoModel); - if (i == 1) { - // 添加积分更变相关记录 - CoinRecords coinRecords = new CoinRecords("取消置顶返还积分",pkInfoModel.getSenderId(),totalCoin, (int) VVTools.currentTimeStamp(),1); - coinRecordsDao.insert(coinRecords); - return ResponseData.success(String.format("操作成功,返还%d积分",totalCoin)); - }else { - return ResponseData.error(ResponseInfo.ERROR.getCode(),null); + if (request == null || request.getArticleId() == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空"); } + int operatorUserId = Integer.parseInt(StpUtil.getLoginId().toString()); + String info = pkPinService.cancelPin(operatorUserId, request.getArticleId()); + return ResponseData.success(info); } diff --git a/src/main/java/vvpkassistant/pk/service/PkPinService.java b/src/main/java/vvpkassistant/pk/service/PkPinService.java new file mode 100644 index 0000000..ef05aaf --- /dev/null +++ b/src/main/java/vvpkassistant/pk/service/PkPinService.java @@ -0,0 +1,8 @@ +package vvpkassistant.pk.service; + +public interface PkPinService { + String pinToTop(int operatorUserId, int articleId, int pinExpireTime); + + String cancelPin(int operatorUserId, int articleId); +} + diff --git a/src/main/java/vvpkassistant/pk/service/PkPinServiceImpl.java b/src/main/java/vvpkassistant/pk/service/PkPinServiceImpl.java new file mode 100644 index 0000000..1233e14 --- /dev/null +++ b/src/main/java/vvpkassistant/pk/service/PkPinServiceImpl.java @@ -0,0 +1,167 @@ +package vvpkassistant.pk.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vvpkassistant.CoinRecords.CoinRecords; +import vvpkassistant.CoinRecords.CoinRecordsDao; +import vvpkassistant.Tools.EpochSecondProvider; +import vvpkassistant.Tools.VVTools; +import vvpkassistant.User.mapper.UserDao; +import vvpkassistant.User.model.UserModel; +import vvpkassistant.common.ErrorCode; +import vvpkassistant.config.FunctionConfigProvider; +import vvpkassistant.exception.BusinessException; +import vvpkassistant.pk.mapper.PkInfoDao; +import vvpkassistant.pk.model.PkInfoModel; + +@Service +public class PkPinServiceImpl implements PkPinService { + + private static final String PIN_COIN_CONFIG_NAME = "置顶扣除积分"; + private static final int COIN_RECORD_ADD = 1; + private static final int COIN_RECORD_DEDUCT = 0; + + private final PkInfoDao pkInfoDao; + private final UserDao userDao; + private final CoinRecordsDao coinRecordsDao; + private final FunctionConfigProvider functionConfigProvider; + private final EpochSecondProvider epochSecondProvider; + + public PkPinServiceImpl( + PkInfoDao pkInfoDao, + UserDao userDao, + CoinRecordsDao coinRecordsDao, + FunctionConfigProvider functionConfigProvider, + EpochSecondProvider epochSecondProvider + ) { + this.pkInfoDao = pkInfoDao; + this.userDao = userDao; + this.coinRecordsDao = coinRecordsDao; + this.functionConfigProvider = functionConfigProvider; + this.epochSecondProvider = epochSecondProvider; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String pinToTop(int operatorUserId, int articleId, int pinExpireTime) { + long now = epochSecondProvider.nowEpochSecond(); + ensureExpireTimeValid(pinExpireTime, now); + + PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId); + if (pkInfoModel == null) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该信息不存在"); + } + ensureOwner(operatorUserId, pkInfoModel.getSenderId()); + + int costPerHour = getPinCoinPerHour(); + long hours = VVTools.calculateHoursRound(pinExpireTime, now); + int totalCoin = toIntCoin(multiplyCoin(costPerHour, hours, "置顶时长过长"), "置顶时长过长"); + + ensureDeductPoints(operatorUserId, totalCoin); + updatePinTime(pkInfoModel, pinExpireTime, (int) now); + insertCoinRecord("置顶扣除积分", operatorUserId, totalCoin, (int) now, COIN_RECORD_DEDUCT); + + return String.format("置顶成功,扣除%d积分", totalCoin); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String cancelPin(int operatorUserId, int articleId) { + long now = epochSecondProvider.nowEpochSecond(); + + PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId); + if (pkInfoModel == null) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该信息不存在"); + } + ensureOwner(operatorUserId, pkInfoModel.getSenderId()); + + int costPerHour = getPinCoinPerHour(); + int pinExpireTime = pkInfoModel.getPinExpireTime() == null ? 0 : pkInfoModel.getPinExpireTime(); + long hours = VVTools.calculateHoursFloor(pinExpireTime, now); + int refundCoin = toIntCoin(multiplyCoin(costPerHour, hours, "返还积分计算溢出"), "返还积分计算溢出"); + + if (refundCoin > 0) { + int updated = userDao.increasePoints(operatorUserId, refundCoin); + if (updated != 1) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户不存在"); + } + } + + pkInfoModel.setPinExpireTime(0); + pkInfoModel.setPinCreateTime(0); + if (pkInfoDao.updateById(pkInfoModel) != 1) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR); + } + + insertCoinRecord("取消置顶返还积分", operatorUserId, refundCoin, (int) now, COIN_RECORD_ADD); + return String.format("操作成功,返还%d积分", refundCoin); + } + + private static void ensureExpireTimeValid(int pinExpireTime, long now) { + if (pinExpireTime <= now) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "置顶到期时间必须大于当前时间"); + } + } + + private static void ensureOwner(int operatorUserId, Integer senderId) { + if (senderId == null || senderId != operatorUserId) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "无权限操作"); + } + } + + private int getPinCoinPerHour() { + String value = functionConfigProvider.getValue(PIN_COIN_CONFIG_NAME); + if (value == null) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "未配置置顶扣除积分"); + } + int coin; + try { + coin = Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "置顶扣除积分配置错误"); + } + if (coin <= 0) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "置顶扣除积分配置错误"); + } + return coin; + } + + private static long multiplyCoin(int costPerHour, long hours, String overflowMessage) { + try { + return Math.multiplyExact((long) costPerHour, hours); + } catch (ArithmeticException e) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, overflowMessage); + } + } + + private static int toIntCoin(long coin, String overflowMessage) { + if (coin > Integer.MAX_VALUE) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, overflowMessage); + } + return (int) coin; + } + + private void ensureDeductPoints(int operatorUserId, int totalCoin) { + int updated = userDao.decreasePointsIfEnough(operatorUserId, totalCoin); + if (updated == 1) { + return; + } + UserModel userModel = userDao.selectById(operatorUserId); + if (userModel == null) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户不存在"); + } + throw new BusinessException(ErrorCode.SYSTEM_ERROR, String.format("积分不足,需要%d积分", totalCoin)); + } + + private void updatePinTime(PkInfoModel pkInfoModel, int pinExpireTime, int now) { + pkInfoModel.setPinExpireTime(pinExpireTime); + pkInfoModel.setPinCreateTime(now); + if (pkInfoDao.updateById(pkInfoModel) != 1) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR); + } + } + + private void insertCoinRecord(String desc, int userId, int coin, int now, int type) { + coinRecordsDao.insert(new CoinRecords(desc, userId, coin, now, type)); + } +} diff --git a/src/test/java/vvpkassistant/pk/service/PkPinServiceImplTests.java b/src/test/java/vvpkassistant/pk/service/PkPinServiceImplTests.java new file mode 100644 index 0000000..fb9b5f5 --- /dev/null +++ b/src/test/java/vvpkassistant/pk/service/PkPinServiceImplTests.java @@ -0,0 +1,109 @@ +package vvpkassistant.pk.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import vvpkassistant.CoinRecords.CoinRecordsDao; +import vvpkassistant.Tools.EpochSecondProvider; +import vvpkassistant.User.mapper.UserDao; +import vvpkassistant.User.model.UserModel; +import vvpkassistant.common.ErrorCode; +import vvpkassistant.config.FunctionConfigProvider; +import vvpkassistant.exception.BusinessException; +import vvpkassistant.pk.mapper.PkInfoDao; +import vvpkassistant.pk.model.PkInfoModel; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class PkPinServiceImplTests { + + @Test + void shouldFailWhenExpireTimeNotInFuture() { + PkPinServiceImpl service = newServiceWithNow(100L); + BusinessException ex = Assertions.assertThrows(BusinessException.class, + () -> service.pinToTop(1, 10, 100)); + Assertions.assertEquals(ErrorCode.PARAMS_ERROR.getCode(), ex.getCode()); + } + + @Test + void shouldFailWhenPointsNotEnoughEvenIfLessThanOneHour() { + long now = 1_000L; + + PkInfoDao pkInfoDao = mock(PkInfoDao.class); + UserDao userDao = mock(UserDao.class); + CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class); + FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class); + EpochSecondProvider epochSecondProvider = () -> now; + + when(functionConfigProvider.getValue("置顶扣除积分")).thenReturn("10"); + + PkInfoModel pkInfoModel = new PkInfoModel(); + pkInfoModel.setId(10); + pkInfoModel.setSenderId(1); + when(pkInfoDao.selectById(10)).thenReturn(pkInfoModel); + + when(userDao.decreasePointsIfEnough(1, 10)).thenReturn(0); + UserModel userModel = new UserModel(); + userModel.setId(1); + userModel.setPoints(9); + when(userDao.selectById(1)).thenReturn(userModel); + + PkPinServiceImpl service = new PkPinServiceImpl( + pkInfoDao, userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider + ); + + int pinExpireTime = (int) (now + 1); + BusinessException ex = Assertions.assertThrows(BusinessException.class, + () -> service.pinToTop(1, 10, pinExpireTime)); + Assertions.assertTrue(ex.getMessage().contains("积分不足")); + + verify(pkInfoDao, never()).updateById(any()); + verify(coinRecordsDao, never()).insert(any()); + } + + @Test + void shouldSucceedAndChargeOneHourWhenJustOneSecond() { + long now = 2_000L; + + PkInfoDao pkInfoDao = mock(PkInfoDao.class); + UserDao userDao = mock(UserDao.class); + CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class); + FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class); + EpochSecondProvider epochSecondProvider = () -> now; + + when(functionConfigProvider.getValue("置顶扣除积分")).thenReturn("10"); + + PkInfoModel pkInfoModel = new PkInfoModel(); + pkInfoModel.setId(10); + pkInfoModel.setSenderId(1); + when(pkInfoDao.selectById(10)).thenReturn(pkInfoModel); + + when(userDao.decreasePointsIfEnough(1, 10)).thenReturn(1); + when(pkInfoDao.updateById(any())).thenReturn(1); + when(coinRecordsDao.insert(any())).thenReturn(1); + + PkPinServiceImpl service = new PkPinServiceImpl( + pkInfoDao, userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider + ); + + int pinExpireTime = (int) (now + 1); + String msg = service.pinToTop(1, 10, pinExpireTime); + Assertions.assertTrue(msg.contains("扣除10积分")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PkInfoModel.class); + verify(pkInfoDao).updateById(captor.capture()); + Assertions.assertEquals(Integer.valueOf(pinExpireTime), captor.getValue().getPinExpireTime()); + Assertions.assertEquals(Integer.valueOf((int) now), captor.getValue().getPinCreateTime()); + } + + private static PkPinServiceImpl newServiceWithNow(long now) { + PkInfoDao pkInfoDao = mock(PkInfoDao.class); + UserDao userDao = mock(UserDao.class); + CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class); + FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class); + EpochSecondProvider epochSecondProvider = () -> now; + return new PkPinServiceImpl(pkInfoDao, userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider); + } +} +