Compare commits

...

4 Commits

Author SHA1 Message Date
irving
6153e6e4f1 fix: 创建提现时不再校验收款码确认时效
Some checks failed
Build and Push Backend / docker (push) Failing after 4s
2025-10-18 21:05:22 -04:00
irving
d681635394 adjust logic, avoide race condition 2025-10-18 21:05:22 -04:00
irving
b9779e7436 feat: 店员收益明细支持条件筛选并返回订单信息 2025-10-18 21:05:22 -04:00
huchuansai
07a86fbe66 fix: 用æ店员头像bug 2025-10-16 16:56:44 +08:00
4 changed files with 146 additions and 16 deletions

View File

@@ -3,6 +3,7 @@ package com.starry.admin.modules.weichat.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.common.oss.service.IOssFileService;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
@@ -14,9 +15,11 @@ import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.ConvertUtil;
import com.starry.common.utils.IdUtils;
import java.io.InputStream;
import java.util.Date;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
@@ -68,12 +71,16 @@ public class WxOauthService {
if (item == null) {
PlayClerkUserInfoEntity entity = ConvertUtil.entityToVo(userInfo, PlayClerkUserInfoEntity.class);
entity.setAvatar(generateAvatar(userInfo.getHeadImgUrl()));
entity.setWeiChatAvatar(generateAvatar(userInfo.getHeadImgUrl()));
entity.setWeiChatAvatar(entity.getAvatar());
entity.setId(IdUtils.getUuid());
entity.setLevelId(playClerkLevelInfoService.getDefaultLevel().getId());
clerkUserInfoService.create(entity);
return entity.getId();
} else {
if (StrUtil.isEmpty(item.getAvatar())) {
clerkUserInfoService.update(Wrappers.lambdaUpdate(PlayClerkUserInfoEntity.class).eq(PlayClerkUserInfoEntity::getId, item.getId())
.set(PlayClerkUserInfoEntity::getAvatar, generateAvatar(userInfo.getHeadImgUrl())));
}
return item.getId();
}
}

View File

@@ -6,16 +6,27 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.starry.admin.common.aspect.ClerkUserLogin;
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
import com.starry.admin.modules.withdraw.vo.ClerkEarningLineVo;
import com.starry.admin.modules.withdraw.vo.ClerkWithdrawBalanceVo;
import com.starry.common.result.TypedR;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +43,8 @@ public class WxWithdrawController {
private IWithdrawalService withdrawalService;
@Resource
private IWithdrawalLogService withdrawalLogService;
@Resource
private IPlayOrderInfoService orderInfoService;
@Data
public static class CreateWithdrawRequest {
@@ -51,18 +64,69 @@ public class WxWithdrawController {
@ClerkUserLogin
@GetMapping("/earnings")
public TypedR<java.util.List<EarningsLineEntity>> listEarnings(@RequestParam(value = "status", required = false) String status,
public TypedR<java.util.List<ClerkEarningLineVo>> listEarnings(
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "pageNum", defaultValue = "1") long pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") long pageSize) {
@RequestParam(value = "pageSize", defaultValue = "10") long pageSize,
@RequestParam(value = "beginTime", required = false) String beginTime,
@RequestParam(value = "endTime", required = false) String endTime) {
String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId();
LambdaQueryWrapper<EarningsLineEntity> q = new LambdaQueryWrapper<>();
q.eq(EarningsLineEntity::getClerkId, clerkId);
if (status != null && !status.isEmpty()) {
q.eq(EarningsLineEntity::getStatus, status);
}
LocalDateTime begin = parseDateTime(beginTime);
LocalDateTime end = parseDateTime(endTime);
if (begin != null) {
q.ge(EarningsLineEntity::getCreatedTime, begin);
}
if (end != null) {
q.le(EarningsLineEntity::getCreatedTime, end);
}
q.orderByDesc(EarningsLineEntity::getCreatedTime);
IPage<EarningsLineEntity> page = earningsService.page(new Page<>(pageNum, pageSize), q);
return TypedR.okPage(page);
List<EarningsLineEntity> records = page.getRecords();
List<ClerkEarningLineVo> vos = new ArrayList<>();
if (!records.isEmpty()) {
List<String> orderIds = records.stream()
.map(EarningsLineEntity::getOrderId)
.filter(id -> id != null && !id.isEmpty())
.distinct()
.collect(Collectors.toList());
Map<String, PlayOrderInfoEntity> orderMap = orderIds.isEmpty() ? java.util.Collections.emptyMap()
: orderInfoService.lambdaQuery()
.in(PlayOrderInfoEntity::getId, orderIds)
.list()
.stream()
.collect(Collectors.toMap(PlayOrderInfoEntity::getId, it -> it));
for (EarningsLineEntity line : records) {
ClerkEarningLineVo vo = new ClerkEarningLineVo();
vo.setId(line.getId());
vo.setAmount(line.getAmount());
vo.setStatus(line.getStatus());
vo.setEarningType(line.getEarningType());
vo.setWithdrawalId(line.getWithdrawalId());
vo.setUnlockTime(line.getUnlockTime());
vo.setCreatedTime(toLocalDateTime(line.getCreatedTime()));
vo.setOrderId(line.getOrderId());
if (line.getOrderId() != null) {
PlayOrderInfoEntity order = orderMap.get(line.getOrderId());
if (order != null) {
vo.setOrderNo(order.getOrderNo());
vo.setOrderStatus(order.getOrderStatus());
vo.setOrderEndTime(toLocalDateTime(order.getOrderEndTime()));
}
}
vos.add(vo);
}
}
Page<ClerkEarningLineVo> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
long totalPage = page.getSize() == 0 ? 0 : (long) Math.ceil((double) page.getTotal() / (double) page.getSize());
result.setPages(totalPage);
result.setRecords(vos);
return TypedR.okPage(result);
}
@ClerkUserLogin
@@ -101,4 +165,36 @@ public class WxWithdrawController {
.list();
return TypedR.ok(list);
}
private LocalDateTime parseDateTime(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
String trimmed = value.trim();
DateTimeFormatter[] formatters = new DateTimeFormatter[] {
DateTimeFormatter.ISO_DATE_TIME,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")
};
for (DateTimeFormatter formatter : formatters) {
try {
return LocalDateTime.parse(trimmed, formatter);
} catch (DateTimeParseException ignored) {
}
}
return null;
}
private LocalDateTime toLocalDateTime(Object value) {
if (value == null) {
return null;
}
if (value instanceof LocalDateTime) {
return (LocalDateTime) value;
}
if (value instanceof Date) {
return ((Date) value).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
return null;
}
}

View File

@@ -28,8 +28,6 @@ import org.springframework.util.StringUtils;
public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper, WithdrawalRequestEntity>
implements IWithdrawalService {
private static final long PAYEE_CONFIRMATION_MAX_MINUTES = 10L;
@Resource
private IEarningsService earningsService;
@Resource
@@ -49,21 +47,17 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
if (payeeProfile == null || !StringUtils.hasText(payeeProfile.getQrCodeUrl())) {
throw new CustomException("请先上传支付宝收款码");
}
if (payeeProfile.getLastConfirmedAt() == null) {
throw new CustomException("请确认本次使用的收款码");
}
Duration sinceConfirm = Duration.between(payeeProfile.getLastConfirmedAt(), now);
if (sinceConfirm.isNegative() || sinceConfirm.toMinutes() > PAYEE_CONFIRMATION_MAX_MINUTES) {
throw new CustomException("收款码确认已过期,请重新确认");
}
BigDecimal available = earningsService.getAvailableAmount(clerkId, now);
if (available.compareTo(amount) < 0) {
throw new CustomException("可提现余额不足");
}
// pick and reserve lines
List<EarningsLineEntity> lines = earningsService.findWithdrawable(clerkId, amount, now);
if (lines.isEmpty()) throw new CustomException("可提现余额不足");
if (lines.isEmpty()) {
BigDecimal latestAvailable = earningsService.getAvailableAmount(clerkId, now);
throw new CustomException("可提现余额不足或已被锁定,当前可用:" + latestAvailable);
}
// Reserve lines FIRST with temp ID (fail fast before creating request)
String tempWithdrawalId = "TEMP_" + IdUtils.getUuid();

View File

@@ -0,0 +1,33 @@
package com.starry.admin.modules.withdraw.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.starry.admin.modules.withdraw.enums.EarningsType;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@Data
public class ClerkEarningLineVo {
private String id;
private BigDecimal amount;
private String status;
private EarningsType earningType;
private String withdrawalId;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime unlockTime;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createdTime;
private String orderId;
private String orderNo;
private String orderStatus;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime orderEndTime;
}