package com.izouma.nineth.service; import com.izouma.nineth.annotations.Debounce; import com.izouma.nineth.config.RedisKeys; import com.izouma.nineth.domain.Asset; import com.izouma.nineth.domain.AuctionActivity; import com.izouma.nineth.domain.User; import com.izouma.nineth.dto.PageQuery; import com.izouma.nineth.dto.auction.AuctionInputDTO; import com.izouma.nineth.enums.*; import com.izouma.nineth.exception.BusinessException; import com.izouma.nineth.repo.*; import com.izouma.nineth.utils.JpaUtils; import com.izouma.nineth.utils.SecurityUtils; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.springframework.core.env.Environment; import org.springframework.data.domain.Page; import org.springframework.data.redis.core.BoundValueOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.TaskScheduler; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.*; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @Slf4j @Service @AllArgsConstructor public class AuctionActivityService { private final AuctionActivityRepo auctionActivityRepo; private final AssetRepo assetRepo; private final UserRepo userRepo; private final PasswordEncoder passwordEncoder; private final RedisTemplate redisTemplate; private final CacheService cacheService; private final TaskScheduler taskScheduler; private final Environment env; private final TokenHistoryRepo tokenHistoryRepo; private final SysConfigService sysConfigService; private final AuctionRecordRepo auctionRecordRepo; private final Map> tasks = new HashMap<>(); @PostConstruct public void init() { if (Arrays.asList(env.getActiveProfiles()).contains("dev")) { return; } List activities = auctionActivityRepo.findByStartTimeBeforeAndStatus(LocalDateTime.now(), AuctionStatus.NOTSTARTED); for (AuctionActivity activity : activities) { onShelfTask(activity); } } public Page all(PageQuery pageQuery) { return auctionActivityRepo .findAll(JpaUtils.toSpecification(pageQuery, AuctionActivity.class), JpaUtils.toPageRequest(pageQuery)); } public AuctionActivity createFromAsset(AuctionInputDTO dto) { Asset asset = assetRepo.findById(dto.getAssetId()).orElseThrow(new BusinessException("暂无")); //拍卖周期 int auctionCycle = sysConfigService.getInt("auction_cycle"); AuctionActivity auctionActivity = new AuctionActivity(); auctionActivity.setAuctionType(AuctionType.NFT); auctionActivity.setAssetId(dto.getAssetId()); auctionActivity.setStatus(AuctionStatus.ONGOING); auctionActivity.setBids(0); auctionActivity.setCategory(asset.getCategory()); auctionActivity.setEndTime(LocalDateTime.now().plusDays(auctionCycle)); auctionActivity.setDeposit(dto.getDeposit()); if (Arrays.asList(env.getActiveProfiles()).contains("staging")) { auctionActivity.setEndTime(LocalDateTime.now().plusMinutes(5)); } auctionActivity.setDetail(asset.getDetail()); auctionActivity.setFixedPrice(dto.getFixedPrice()); auctionActivity.setIncrement(dto.getIncrement()); auctionActivity.setMinter(asset.getMinter()); auctionActivity.setPic(asset.getPic()); auctionActivity.setModel3d(asset.getModel3d()); auctionActivity.setName(asset.getName()); auctionActivity.setSeller(asset.getOwner()); auctionActivity.setSellerId(asset.getOwnerId()); auctionActivity.setStartTime(LocalDateTime.now()); auctionActivity.setSource(AuctionSource.TRANSFER); //固定值 or 资产值 auctionActivity.setServiceCharge(asset.getServiceCharge()); auctionActivity.setRoyalties(asset.getRoyalties()); auctionActivity.setStartingPrice(dto.getStartingPrice()); auctionActivity.setHasFixedPrice(auctionActivity.getFixedPrice() != null); return save(auctionActivity, dto.getTradeCode()); } public AuctionActivity save(AuctionActivity record, String tradeCode) { if (record.getSource().equals(AuctionSource.OFFICIAL)) { record.setStatus(AuctionStatus.NOTSTARTED); } if (record.getSource().equals(AuctionSource.TRANSFER) && !record.getSellerId().equals(9859L)) { User user = userRepo.findById(record.getSellerId()).orElseThrow(new BusinessException("无用户信息")); if (!passwordEncoder.matches(tradeCode, user.getTradeCode())) { throw new BusinessException("交易密码错误"); } if (!AuthStatus.SUCCESS.equals(user.getAuthStatus())) { throw new BusinessException("未实名或实名未通过"); } BigDecimal userBuy = tokenHistoryRepo.userBuy(record.getSellerId()); BigDecimal num = sysConfigService.getBigDecimal("auction_lvzhoushi_num"); if (userBuy.compareTo(num) < 0) { throw new BusinessException("绿洲石不足"); } Asset asset = assetRepo.findById(record.getAssetId()).orElseThrow(new BusinessException("未找到该藏品")); if (!asset.getOwnerId().equals(SecurityUtils.getAuthenticatedUser().getId())) { throw new BusinessException("非本人藏品,无法操作."); } if (!asset.getStatus().equals(AssetStatus.NORMAL)) { throw new BusinessException("藏品状态异常,无法操作."); } if (asset.isPublicShow() || asset.isConsignment()) { // throw new BusinessException("藏品已寄售,取消寄售后再申请拍卖。"); throw new BusinessException("请先下架藏品"); } //是否二次拍卖 AuctionActivity activity = auctionActivityRepo.findByAssetId(asset.getId()); if (ObjectUtils.isNotEmpty(activity)) { if (!AuctionStatus.PASS.equals(activity.getStatus())) { throw new BusinessException("已有拍卖"); } log.info("删除流拍拍卖:id-{},assetId-{}", activity.getId(), activity.getAssetId()); auctionActivityRepo.delete(activity); auctionRecordRepo.deleteAllByAuctionId(activity.getId()); } asset.setStatus(AssetStatus.AUCTIONING); assetRepo.save(asset); } AuctionActivity saved = auctionActivityRepo.save(record); if (saved.getStatus().equals(AuctionStatus.NOTSTARTED)) { onShelfTask(saved); } else if (saved.getStatus().equals(AuctionStatus.ONGOING)) { offShelfTask(saved); } return saved; } public synchronized String changeStatus(Long id, AuctionStatus status) { BoundValueOperations ops = redisTemplate.boundValueOps(RedisKeys.AUCTION_STATUS + id); if (ops.get() == null) { Boolean success = ops.setIfAbsent(Optional.ofNullable(auctionActivityRepo.getStatus(id)) .orElse(AuctionStatus.NOTSTARTED.toString()), 7, TimeUnit.DAYS); log.info("创建redis拍卖活动状态:{}", success); } String stock = (String) ops.getAndSet(status.toString()); syncStatus(id); return stock; } @Debounce(key = "#id", delay = 500) public void syncStatus(Long id) { String stock = (String) redisTemplate.opsForValue().get(RedisKeys.AUCTION_STATUS + id); if (stock != null) { log.info("同步拍卖活动状态信息{}", id); auctionActivityRepo.updateStatus(id, AuctionStatus.valueOf(stock)); cacheService.clearAuction(id); } } private void onShelfTask(AuctionActivity record) { ScheduledFuture task = tasks.get(record.getId()); if (task != null) { if (!task.cancel(true)) { return; } } if (record.getStatus().equals(AuctionStatus.NOTSTARTED)) { if (record.getStartTime().minusSeconds(2).isAfter(LocalDateTime.now())) { Date date = Date.from(record.getStartTime().atZone(ZoneId.systemDefault()).toInstant()); ScheduledFuture future = taskScheduler.schedule(() -> { auctionActivityRepo.scheduleOnShelf(record.getId(), AuctionStatus.ONGOING); tasks.remove(record.getId()); offShelfTask(record); }, date); tasks.put(record.getId(), future); } else { auctionActivityRepo.scheduleOnShelf(record.getId(), AuctionStatus.ONGOING); offShelfTask(record); } } } private void offShelfTask(AuctionActivity record) { ScheduledFuture task = tasks.get(record.getId()); if (task != null) { if (!task.cancel(true)) { return; } } AuctionActivity recordNew1 = auctionActivityRepo.findById(record.getId()) .orElseThrow(new BusinessException("无数据")); if (recordNew1.getStatus().equals(AuctionStatus.ONGOING)) { if (recordNew1.getEndTime().minusSeconds(2).isAfter(LocalDateTime.now())) { Date date = Date.from(record.getEndTime().atZone(ZoneId.systemDefault()).toInstant()); ScheduledFuture future = taskScheduler.schedule(() -> { AuctionActivity nowRecord = auctionActivityRepo.findById(record.getId()) .orElseThrow(new BusinessException("无数据")); if (nowRecord.getPurchasePrice() != null) { auctionActivityRepo.scheduleOffShelf(nowRecord.getId(), AuctionStatus.PURCHASED); } else { auctionActivityRepo.scheduleOffShelf(nowRecord.getId(), AuctionStatus.PASS); if (record.getAuctionType().equals(AuctionType.NFT)) { Asset asset = assetRepo.findById(nowRecord.getAssetId()) .orElseThrow(new BusinessException("暂无")); asset.setStatus(AssetStatus.NORMAL); asset.setConsignment(false); asset.setPublicShow(false); assetRepo.save(asset); } } tasks.remove(record.getId()); }, date); tasks.put(record.getId(), future); } else { if (recordNew1.getPurchasePrice() != null) { auctionActivityRepo.scheduleOffShelf(recordNew1.getId(), AuctionStatus.PURCHASED); } else { auctionActivityRepo.scheduleOffShelf(recordNew1.getId(), AuctionStatus.PASS); if (record.getAuctionType().equals(AuctionType.NFT)) { Asset asset = assetRepo.findById(recordNew1.getAssetId()) .orElseThrow(new BusinessException("暂无")); asset.setStatus(AssetStatus.NORMAL); asset.setConsignment(false); asset.setPublicShow(false); assetRepo.save(asset); } } } } } }