Compare commits
2 Commits
8faa23e9c3
...
0725c94bbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0725c94bbe | ||
|
|
5438a8e1f0 |
@@ -3,15 +3,20 @@ package com.starry.admin.modules.withdraw.controller;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
|
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsBackfillLogService;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsBackfillService;
|
||||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
import com.starry.admin.modules.withdraw.service.ITenantAlipayConfigService;
|
import com.starry.admin.modules.withdraw.service.ITenantAlipayConfigService;
|
||||||
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
||||||
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
|
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
|
||||||
import com.starry.admin.modules.withdraw.vo.EarningsAdminQueryVo;
|
import com.starry.admin.modules.withdraw.vo.EarningsAdminQueryVo;
|
||||||
import com.starry.admin.modules.withdraw.vo.EarningsAdminSummaryVo;
|
import com.starry.admin.modules.withdraw.vo.EarningsAdminSummaryVo;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.EarningsBackfillRequest;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.EarningsBackfillResponse;
|
||||||
import com.starry.admin.modules.withdraw.vo.WithdrawalRequestQueryVo;
|
import com.starry.admin.modules.withdraw.vo.WithdrawalRequestQueryVo;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.common.result.TypedR;
|
import com.starry.common.result.TypedR;
|
||||||
@@ -40,6 +45,10 @@ public class AdminWithdrawalController {
|
|||||||
private ITenantAlipayConfigService tenantAlipayConfigService;
|
private ITenantAlipayConfigService tenantAlipayConfigService;
|
||||||
@Resource
|
@Resource
|
||||||
private IEarningsService earningsService;
|
private IEarningsService earningsService;
|
||||||
|
@Resource
|
||||||
|
private IEarningsBackfillService earningsBackfillService;
|
||||||
|
@Resource
|
||||||
|
private IEarningsBackfillLogService backfillLogService;
|
||||||
|
|
||||||
@ApiOperation("分页查询提现请求")
|
@ApiOperation("分页查询提现请求")
|
||||||
@PostMapping("/requests/listByPage")
|
@PostMapping("/requests/listByPage")
|
||||||
@@ -114,6 +123,23 @@ public class AdminWithdrawalController {
|
|||||||
return TypedR.ok(summary);
|
return TypedR.ok(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation("收益补算(试运行或执行)")
|
||||||
|
@PostMapping("/earnings/backfill")
|
||||||
|
public TypedR<EarningsBackfillResponse> backfill(@RequestBody EarningsBackfillRequest request) {
|
||||||
|
return TypedR.ok(earningsBackfillService.backfill(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("收益补算历史记录")
|
||||||
|
@GetMapping("/earnings/backfill/logs")
|
||||||
|
public TypedR<List<EarningsBackfillLogEntity>> listBackfillLogs() {
|
||||||
|
List<EarningsBackfillLogEntity> logs = backfillLogService.lambdaQuery()
|
||||||
|
.eq(EarningsBackfillLogEntity::getTenantId, SecurityUtils.getTenantId())
|
||||||
|
.orderByDesc(EarningsBackfillLogEntity::getCreatedTime)
|
||||||
|
.last("limit 20")
|
||||||
|
.list();
|
||||||
|
return TypedR.ok(logs);
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOperation("查询当前租户是否已配置支付宝")
|
@ApiOperation("查询当前租户是否已配置支付宝")
|
||||||
@GetMapping("/alipay/config/present")
|
@GetMapping("/alipay/config/present")
|
||||||
public TypedR<Boolean> hasAlipayConfig() {
|
public TypedR<Boolean> hasAlipayConfig() {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ public class WxWithdrawController {
|
|||||||
@Data
|
@Data
|
||||||
public static class CreateWithdrawRequest {
|
public static class CreateWithdrawRequest {
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
private String destAccount; // 临时:支付宝登录号/账号
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ClerkUserLogin
|
@ClerkUserLogin
|
||||||
@@ -73,8 +72,7 @@ public class WxWithdrawController {
|
|||||||
throw new CustomException("提现金额必须大于0");
|
throw new CustomException("提现金额必须大于0");
|
||||||
}
|
}
|
||||||
String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId();
|
String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId();
|
||||||
WithdrawalRequestEntity req = withdrawalService.createWithdrawaRequest(clerkId, body.getDestAccount(),
|
WithdrawalRequestEntity req = withdrawalService.createWithdrawaRequest(clerkId, body.getAmount());
|
||||||
body.getAmount());
|
|
||||||
return TypedR.ok(req);
|
return TypedR.ok(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.controller;
|
||||||
|
|
||||||
|
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.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IClerkPayeeProfileService;
|
||||||
|
import com.starry.common.result.TypedR;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
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.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/wx/withdraw/payee")
|
||||||
|
public class WxWithdrawPayeeController {
|
||||||
|
|
||||||
|
private static final String DEFAULT_CHANNEL = "ALIPAY_QR";
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IClerkPayeeProfileService clerkPayeeProfileService;
|
||||||
|
|
||||||
|
@ClerkUserLogin
|
||||||
|
@GetMapping
|
||||||
|
public TypedR<PayeeProfileResponse> getProfile() {
|
||||||
|
PlayClerkUserInfoEntity clerk = ThreadLocalRequestDetail.getClerkUserInfo();
|
||||||
|
ClerkPayeeProfileEntity profile = clerkPayeeProfileService.getByClerk(clerk.getTenantId(), clerk.getId());
|
||||||
|
return TypedR.ok(toResponse(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ClerkUserLogin
|
||||||
|
@PostMapping
|
||||||
|
public TypedR<PayeeProfileResponse> upsert(@RequestBody PayeeProfileRequest body) {
|
||||||
|
if (body == null || !StringUtils.hasText(body.getQrCodeUrl())) {
|
||||||
|
throw new CustomException("请上传支付宝收款二维码");
|
||||||
|
}
|
||||||
|
PlayClerkUserInfoEntity clerk = ThreadLocalRequestDetail.getClerkUserInfo();
|
||||||
|
String channel = StringUtils.hasText(body.getChannel()) ? body.getChannel() : DEFAULT_CHANNEL;
|
||||||
|
String displayName = StringUtils.hasText(body.getDisplayName()) ? body.getDisplayName() : clerk.getNickname();
|
||||||
|
ClerkPayeeProfileEntity profile = clerkPayeeProfileService.upsertProfile(
|
||||||
|
clerk.getTenantId(), clerk.getId(), channel, body.getQrCodeUrl(), displayName, clerk.getId());
|
||||||
|
return TypedR.ok(toResponse(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ClerkUserLogin
|
||||||
|
@PostMapping("/confirm")
|
||||||
|
public TypedR<PayeeProfileResponse> confirm() {
|
||||||
|
PlayClerkUserInfoEntity clerk = ThreadLocalRequestDetail.getClerkUserInfo();
|
||||||
|
ClerkPayeeProfileEntity profile = clerkPayeeProfileService.confirmProfile(
|
||||||
|
clerk.getTenantId(), clerk.getId(), clerk.getId());
|
||||||
|
if (profile == null || !StringUtils.hasText(profile.getQrCodeUrl())) {
|
||||||
|
throw new CustomException("请先上传支付宝收款二维码");
|
||||||
|
}
|
||||||
|
return TypedR.ok(toResponse(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayeeProfileResponse toResponse(ClerkPayeeProfileEntity profile) {
|
||||||
|
if (profile == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PayeeProfileResponse resp = new PayeeProfileResponse();
|
||||||
|
resp.setChannel(profile.getChannel());
|
||||||
|
resp.setQrCodeUrl(profile.getQrCodeUrl());
|
||||||
|
resp.setDisplayName(profile.getDisplayName());
|
||||||
|
resp.setLastConfirmedAt(profile.getLastConfirmedAt());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class PayeeProfileRequest {
|
||||||
|
private String channel;
|
||||||
|
private String qrCodeUrl;
|
||||||
|
private String displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class PayeeProfileResponse {
|
||||||
|
private String channel;
|
||||||
|
private String qrCodeUrl;
|
||||||
|
private String displayName;
|
||||||
|
private java.time.LocalDateTime lastConfirmedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.starry.common.domain.BaseEntity;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("play_clerk_payee_profile")
|
||||||
|
public class ClerkPayeeProfileEntity extends BaseEntity<ClerkPayeeProfileEntity> {
|
||||||
|
private String id;
|
||||||
|
private String tenantId;
|
||||||
|
private String clerkId;
|
||||||
|
/**
|
||||||
|
* 收款渠道(目前固定 ALIPAY_QR)
|
||||||
|
*/
|
||||||
|
private String channel;
|
||||||
|
private String qrCodeUrl;
|
||||||
|
private String displayName;
|
||||||
|
private LocalDateTime lastConfirmedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.starry.common.domain.BaseEntity;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("play_earnings_backfill_log")
|
||||||
|
public class EarningsBackfillLogEntity extends BaseEntity<EarningsBackfillLogEntity> {
|
||||||
|
private String id;
|
||||||
|
private String tenantId;
|
||||||
|
private String operatorId;
|
||||||
|
private String operatorName;
|
||||||
|
private LocalDateTime backfillBeginTime;
|
||||||
|
private LocalDateTime backfillEndTime;
|
||||||
|
private String clerkIds;
|
||||||
|
private Boolean dryRun;
|
||||||
|
private Integer ordersChecked;
|
||||||
|
private Integer createdCount;
|
||||||
|
private Integer skippedExisting;
|
||||||
|
private Integer warningCount;
|
||||||
|
private String warnings;
|
||||||
|
private String comment;
|
||||||
|
}
|
||||||
@@ -24,4 +24,5 @@ public class WithdrawalRequestEntity extends BaseEntity<WithdrawalRequestEntity>
|
|||||||
private String outBizNo;
|
private String outBizNo;
|
||||||
private String providerRef;
|
private String providerRef;
|
||||||
private String failureReason;
|
private String failureReason;
|
||||||
|
private String payeeSnapshot;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ClerkPayeeProfileMapper extends BaseMapper<ClerkPayeeProfileEntity> {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface EarningsBackfillLogMapper extends BaseMapper<EarningsBackfillLogEntity> {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
||||||
|
|
||||||
|
public interface IClerkPayeeProfileService extends IService<ClerkPayeeProfileEntity> {
|
||||||
|
ClerkPayeeProfileEntity getByClerk(String tenantId, String clerkId);
|
||||||
|
|
||||||
|
ClerkPayeeProfileEntity upsertProfile(String tenantId, String clerkId, String channel, String qrCodeUrl, String displayName, String operator);
|
||||||
|
|
||||||
|
ClerkPayeeProfileEntity confirmProfile(String tenantId, String clerkId, String operator);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity;
|
||||||
|
|
||||||
|
public interface IEarningsBackfillLogService extends IService<EarningsBackfillLogEntity> {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.withdraw.vo.EarningsBackfillRequest;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.EarningsBackfillResponse;
|
||||||
|
|
||||||
|
public interface IEarningsBackfillService {
|
||||||
|
EarningsBackfillResponse backfill(EarningsBackfillRequest request);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
public interface IWithdrawalService extends IService<WithdrawalRequestEntity> {
|
public interface IWithdrawalService extends IService<WithdrawalRequestEntity> {
|
||||||
WithdrawalRequestEntity createWithdrawaRequest(String clerkId, String destAccount, BigDecimal amount);
|
WithdrawalRequestEntity createWithdrawaRequest(String clerkId, BigDecimal amount);
|
||||||
|
|
||||||
void markManualSuccess(String requestId, String operatorBy);
|
void markManualSuccess(String requestId, String operatorBy);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.mapper.ClerkPayeeProfileMapper;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IClerkPayeeProfileService;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ClerkPayeeProfileServiceImpl extends ServiceImpl<ClerkPayeeProfileMapper, ClerkPayeeProfileEntity>
|
||||||
|
implements IClerkPayeeProfileService {
|
||||||
|
|
||||||
|
private static final String DEFAULT_CHANNEL = "ALIPAY_QR";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClerkPayeeProfileEntity getByClerk(String tenantId, String clerkId) {
|
||||||
|
return lambdaQuery()
|
||||||
|
.eq(ClerkPayeeProfileEntity::getTenantId, tenantId)
|
||||||
|
.eq(ClerkPayeeProfileEntity::getClerkId, clerkId)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClerkPayeeProfileEntity upsertProfile(String tenantId, String clerkId, String channel,
|
||||||
|
String qrCodeUrl, String displayName, String operator) {
|
||||||
|
String resolvedChannel = StringUtils.hasText(channel) ? channel : DEFAULT_CHANNEL;
|
||||||
|
ClerkPayeeProfileEntity profile = getByClerk(tenantId, clerkId);
|
||||||
|
if (profile == null) {
|
||||||
|
profile = new ClerkPayeeProfileEntity();
|
||||||
|
profile.setId(IdUtils.getUuid());
|
||||||
|
profile.setTenantId(tenantId);
|
||||||
|
profile.setClerkId(clerkId);
|
||||||
|
}
|
||||||
|
profile.setChannel(resolvedChannel);
|
||||||
|
profile.setQrCodeUrl(qrCodeUrl);
|
||||||
|
profile.setDisplayName(displayName);
|
||||||
|
profile.setLastConfirmedAt(null); // force confirmation before next withdrawal
|
||||||
|
profile.setUpdatedBy(operator);
|
||||||
|
if (profile.getCreatedBy() == null) {
|
||||||
|
profile.setCreatedBy(operator);
|
||||||
|
}
|
||||||
|
this.saveOrUpdate(profile);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClerkPayeeProfileEntity confirmProfile(String tenantId, String clerkId, String operator) {
|
||||||
|
ClerkPayeeProfileEntity profile = getByClerk(tenantId, clerkId);
|
||||||
|
if (profile == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
profile.setLastConfirmedAt(LocalDateTime.now());
|
||||||
|
profile.setUpdatedBy(operator);
|
||||||
|
updateById(profile);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.mapper.EarningsBackfillLogMapper;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsBackfillLogService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EarningsBackfillLogServiceImpl extends ServiceImpl<EarningsBackfillLogMapper, EarningsBackfillLogEntity>
|
||||||
|
implements IEarningsBackfillLogService {}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
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.EarningsBackfillLogEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsBackfillLogService;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsBackfillService;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.BackfillOrderPreview;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.EarningsBackfillRequest;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.EarningsBackfillResponse;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EarningsBackfillServiceImpl implements IEarningsBackfillService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter ISO_DATE_TIME = DateTimeFormatter.ISO_DATE_TIME;
|
||||||
|
private static final DateTimeFormatter SIMPLE_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayOrderInfoService orderInfoService;
|
||||||
|
@Resource
|
||||||
|
private IEarningsService earningsService;
|
||||||
|
@Resource
|
||||||
|
private IEarningsBackfillLogService backfillLogService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public EarningsBackfillResponse backfill(EarningsBackfillRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new CustomException("参数不能为空");
|
||||||
|
}
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new CustomException("租户上下文为空");
|
||||||
|
}
|
||||||
|
LocalDateTime begin = parseDate(request.getBeginTime());
|
||||||
|
LocalDateTime end = parseDate(request.getEndTime());
|
||||||
|
if (begin != null && end != null && begin.isAfter(end)) {
|
||||||
|
throw new CustomException("起始时间不能晚于结束时间");
|
||||||
|
}
|
||||||
|
boolean dryRun = Boolean.TRUE.equals(request.getDryRun());
|
||||||
|
|
||||||
|
LambdaQueryWrapper<PlayOrderInfoEntity> query = new LambdaQueryWrapper<>();
|
||||||
|
query.eq(PlayOrderInfoEntity::getTenantId, tenantId)
|
||||||
|
.eq(PlayOrderInfoEntity::getOrderStatus, "3");
|
||||||
|
if (begin != null) {
|
||||||
|
query.ge(PlayOrderInfoEntity::getOrderEndTime, begin);
|
||||||
|
}
|
||||||
|
if (end != null) {
|
||||||
|
query.le(PlayOrderInfoEntity::getOrderEndTime, end);
|
||||||
|
}
|
||||||
|
if (!CollectionUtils.isEmpty(request.getClerkIds())) {
|
||||||
|
query.in(PlayOrderInfoEntity::getAcceptBy, request.getClerkIds());
|
||||||
|
}
|
||||||
|
List<PlayOrderInfoEntity> orders = orderInfoService.list(query);
|
||||||
|
if (orders.size() > 2000) {
|
||||||
|
throw new CustomException("符合条件的订单过多,请缩小时间范围或筛选店员后再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
int ordersChecked = 0;
|
||||||
|
int createdCount = 0;
|
||||||
|
int skippedExisting = 0;
|
||||||
|
List<String> warnings = new ArrayList<>();
|
||||||
|
List<BackfillOrderPreview> previews = new ArrayList<>();
|
||||||
|
final int previewLimit = 50;
|
||||||
|
|
||||||
|
for (PlayOrderInfoEntity order : orders) {
|
||||||
|
ordersChecked++;
|
||||||
|
BigDecimal amount = resolveAmount(order, warnings);
|
||||||
|
if (amount == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (hasExistingEarnings(tenantId, order.getId())) {
|
||||||
|
skippedExisting++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (previews.size() < previewLimit) {
|
||||||
|
BackfillOrderPreview preview = new BackfillOrderPreview();
|
||||||
|
preview.setOrderId(order.getId());
|
||||||
|
preview.setClerkId(order.getAcceptBy());
|
||||||
|
preview.setAmount(amount);
|
||||||
|
preview.setOrderEndTime(order.getOrderEndTime());
|
||||||
|
previews.add(preview);
|
||||||
|
}
|
||||||
|
if (dryRun) {
|
||||||
|
createdCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
earningsService.createFromOrder(order);
|
||||||
|
if (hasExistingEarnings(tenantId, order.getId())) {
|
||||||
|
createdCount++;
|
||||||
|
} else {
|
||||||
|
addWarning(warnings, "订单 " + order.getId() + " 创建收益失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EarningsBackfillResponse response = new EarningsBackfillResponse();
|
||||||
|
response.setDryRun(dryRun);
|
||||||
|
response.setOrdersChecked(ordersChecked);
|
||||||
|
response.setCreatedCount(createdCount);
|
||||||
|
response.setSkippedExisting(skippedExisting);
|
||||||
|
response.setWarnings(warnings);
|
||||||
|
response.setOrders(previews);
|
||||||
|
|
||||||
|
// persist log
|
||||||
|
EarningsBackfillLogEntity log = new EarningsBackfillLogEntity();
|
||||||
|
log.setId(IdUtils.getUuid());
|
||||||
|
log.setTenantId(tenantId);
|
||||||
|
log.setOperatorId(safe(SecurityUtils::getUserId));
|
||||||
|
log.setOperatorName(safe(SecurityUtils::getUsername));
|
||||||
|
log.setBackfillBeginTime(begin);
|
||||||
|
log.setBackfillEndTime(end);
|
||||||
|
if (!CollectionUtils.isEmpty(request.getClerkIds())) {
|
||||||
|
log.setClerkIds(JSON.toJSONString(request.getClerkIds()));
|
||||||
|
}
|
||||||
|
log.setDryRun(dryRun);
|
||||||
|
log.setOrdersChecked(ordersChecked);
|
||||||
|
log.setCreatedCount(createdCount);
|
||||||
|
log.setSkippedExisting(skippedExisting);
|
||||||
|
log.setWarningCount(warnings.size());
|
||||||
|
if (!warnings.isEmpty()) {
|
||||||
|
log.setWarnings(JSON.toJSONString(warnings));
|
||||||
|
}
|
||||||
|
log.setComment(request.getComment());
|
||||||
|
log.setCreatedBy(log.getOperatorId());
|
||||||
|
log.setUpdatedBy(log.getOperatorId());
|
||||||
|
backfillLogService.save(log);
|
||||||
|
response.setLogId(log.getId());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasExistingEarnings(String tenantId, String orderId) {
|
||||||
|
Long count = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getTenantId, tenantId)
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.count();
|
||||||
|
return count != null && count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal resolveAmount(PlayOrderInfoEntity order, List<String> warnings) {
|
||||||
|
if (order == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (order.getAcceptBy() == null) {
|
||||||
|
addWarning(warnings, "订单 " + order.getId() + " 无接单人,跳过");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BigDecimal amount = order.getEstimatedRevenue() != null ? order.getEstimatedRevenue()
|
||||||
|
: (order.getFinalAmount() != null ? order.getFinalAmount() : BigDecimal.ZERO);
|
||||||
|
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
addWarning(warnings, "订单 " + order.getId() + " 收益金额为0,跳过");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (order.getOrderEndTime() == null) {
|
||||||
|
addWarning(warnings, "订单 " + order.getId() + " 缺少结束时间,跳过");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addWarning(List<String> warnings, String message) {
|
||||||
|
if (warnings.size() >= 50) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warnings.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime parseDate(String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = value.trim();
|
||||||
|
List<DateTimeFormatter> formatters = new ArrayList<>();
|
||||||
|
formatters.add(ISO_DATE_TIME);
|
||||||
|
formatters.add(SIMPLE_DATE_TIME);
|
||||||
|
for (DateTimeFormatter formatter : formatters) {
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(trimmed, formatter);
|
||||||
|
} catch (DateTimeParseException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new CustomException("无法解析时间:" + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface SupplierWithException<T> {
|
||||||
|
T get() throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safe(SupplierWithException<String> supplier) {
|
||||||
|
try {
|
||||||
|
return supplier.get();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
|||||||
LocalDateTime endTime = orderInfo.getOrderEndTime();
|
LocalDateTime endTime = orderInfo.getOrderEndTime();
|
||||||
if (endTime == null) return;
|
if (endTime == null) return;
|
||||||
LocalDateTime unlockTime = endTime.plusHours(freezeHours);
|
LocalDateTime unlockTime = endTime.plusHours(freezeHours);
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
EarningsLineEntity line = new EarningsLineEntity();
|
EarningsLineEntity line = new EarningsLineEntity();
|
||||||
line.setId(IdUtils.getUuid());
|
line.setId(IdUtils.getUuid());
|
||||||
@@ -45,7 +46,7 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
|||||||
line.setAmount(amount);
|
line.setAmount(amount);
|
||||||
line.setEarningType(EarningsType.ORDER);
|
line.setEarningType(EarningsType.ORDER);
|
||||||
line.setUnlockTime(unlockTime);
|
line.setUnlockTime(unlockTime);
|
||||||
line.setStatus("frozen");
|
line.setStatus(unlockTime.isAfter(now) ? "frozen" : "available");
|
||||||
this.save(line);
|
this.save(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +1,122 @@
|
|||||||
package com.starry.admin.modules.withdraw.service.impl;
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
||||||
import com.starry.admin.modules.withdraw.mapper.WithdrawalRequestMapper;
|
import com.starry.admin.modules.withdraw.mapper.WithdrawalRequestMapper;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IClerkPayeeProfileService;
|
||||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
||||||
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
|
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.PayeeSnapshotVo;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper, WithdrawalRequestEntity>
|
public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper, WithdrawalRequestEntity>
|
||||||
implements IWithdrawalService {
|
implements IWithdrawalService {
|
||||||
|
|
||||||
|
private static final long PAYEE_CONFIRMATION_MAX_MINUTES = 10L;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IEarningsService earningsService;
|
private IEarningsService earningsService;
|
||||||
@Resource
|
@Resource
|
||||||
private IWithdrawalLogService withdrawalLogService;
|
private IWithdrawalLogService withdrawalLogService;
|
||||||
|
@Resource
|
||||||
|
private IClerkPayeeProfileService clerkPayeeProfileService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public WithdrawalRequestEntity createWithdrawaRequest(String clerkId, String destAccount, BigDecimal amount) {
|
public WithdrawalRequestEntity createWithdrawaRequest(String clerkId, BigDecimal amount) {
|
||||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
throw new CustomException("提现金额必须大于0");
|
throw new CustomException("提现金额必须大于0");
|
||||||
}
|
}
|
||||||
BigDecimal available = earningsService.getAvailableAmount(clerkId, LocalDateTime.now());
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
ClerkPayeeProfileEntity payeeProfile = clerkPayeeProfileService.getByClerk(tenantId, clerkId);
|
||||||
|
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) {
|
if (available.compareTo(amount) < 0) {
|
||||||
throw new CustomException("可提现余额不足");
|
throw new CustomException("可提现余额不足");
|
||||||
}
|
}
|
||||||
// pick and reserve lines
|
// pick and reserve lines
|
||||||
List<EarningsLineEntity> lines = earningsService.findWithdrawable(clerkId, amount, LocalDateTime.now());
|
List<EarningsLineEntity> lines = earningsService.findWithdrawable(clerkId, amount, now);
|
||||||
if (lines.isEmpty()) throw new CustomException("可提现余额不足");
|
if (lines.isEmpty()) throw new CustomException("可提现余额不足");
|
||||||
|
|
||||||
|
// Reserve lines FIRST with temp ID (fail fast before creating request)
|
||||||
|
String tempWithdrawalId = "TEMP_" + IdUtils.getUuid();
|
||||||
|
int reservedCount = 0;
|
||||||
|
for (EarningsLineEntity line : lines) {
|
||||||
|
boolean updated = earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||||
|
.eq(EarningsLineEntity::getId, line.getId())
|
||||||
|
.eq(EarningsLineEntity::getStatus, line.getStatus()) // Verify status unchanged
|
||||||
|
.set(EarningsLineEntity::getStatus, "withdrawing")
|
||||||
|
.set(EarningsLineEntity::getWithdrawalId, tempWithdrawalId));
|
||||||
|
if (!updated) {
|
||||||
|
// Another request already took this line
|
||||||
|
throw new CustomException("部分收益已被其他提现请求锁定,请重试");
|
||||||
|
}
|
||||||
|
reservedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only NOW create the request (after successful reservation)
|
||||||
|
LocalDateTime confirmedAt = payeeProfile.getLastConfirmedAt();
|
||||||
|
String snapshotJson = buildPayeeSnapshot(payeeProfile, confirmedAt);
|
||||||
|
|
||||||
WithdrawalRequestEntity req = new WithdrawalRequestEntity();
|
WithdrawalRequestEntity req = new WithdrawalRequestEntity();
|
||||||
req.setId(IdUtils.getUuid());
|
req.setId(IdUtils.getUuid());
|
||||||
req.setClerkId(clerkId);
|
req.setClerkId(clerkId);
|
||||||
req.setTenantId(SecurityUtils.getTenantId());
|
req.setTenantId(tenantId);
|
||||||
req.setAmount(amount);
|
req.setAmount(amount);
|
||||||
req.setFee(BigDecimal.ZERO); // fee on tenant; not deducted from clerk
|
req.setFee(BigDecimal.ZERO); // fee on tenant; not deducted from clerk
|
||||||
req.setNetAmount(amount);
|
req.setNetAmount(amount);
|
||||||
req.setDestAccount(destAccount);
|
req.setDestAccount(payeeProfile.getDisplayName());
|
||||||
|
req.setPayeeSnapshot(snapshotJson);
|
||||||
req.setStatus("pending");
|
req.setStatus("pending");
|
||||||
req.setOutBizNo(req.getId());
|
req.setOutBizNo(req.getId());
|
||||||
this.save(req);
|
this.save(req);
|
||||||
|
|
||||||
|
// Update temp ID to real ID
|
||||||
|
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||||
|
.eq(EarningsLineEntity::getWithdrawalId, tempWithdrawalId)
|
||||||
|
.set(EarningsLineEntity::getWithdrawalId, req.getId()));
|
||||||
|
|
||||||
// log created
|
// log created
|
||||||
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||||
"CREATED", null, req.getStatus(), "提现申请创建", null);
|
"CREATED", null, req.getStatus(), "提现申请创建", null);
|
||||||
|
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||||
int reservedCount = 0;
|
"PAYEE_CONFIRMED", req.getStatus(), req.getStatus(), "店员确认收款码", snapshotJson);
|
||||||
for (EarningsLineEntity line : lines) {
|
|
||||||
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
|
||||||
.eq(EarningsLineEntity::getId, line.getId())
|
|
||||||
.set(EarningsLineEntity::getStatus, "withdrawing")
|
|
||||||
.set(EarningsLineEntity::getWithdrawalId, req.getId()));
|
|
||||||
reservedCount++;
|
|
||||||
}
|
|
||||||
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(),
|
||||||
"RESERVED", req.getStatus(), req.getStatus(),
|
"RESERVED", req.getStatus(), req.getStatus(),
|
||||||
"冻结收益已预留,条数=" + reservedCount + ", 金额=" + amount, null);
|
"冻结收益已预留,条数=" + reservedCount + ", 金额=" + amount, null);
|
||||||
|
|
||||||
|
// reset confirmation to force next withdrawal to re-confirm
|
||||||
|
payeeProfile.setLastConfirmedAt(null);
|
||||||
|
payeeProfile.setUpdatedBy(clerkId);
|
||||||
|
clerkPayeeProfileService.updateById(payeeProfile);
|
||||||
|
|
||||||
// 自动打款未实现,等待运营手动处理
|
// 自动打款未实现,等待运营手动处理
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
@@ -116,4 +165,13 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
// Not implemented yet
|
// Not implemented yet
|
||||||
throw new UnsupportedOperationException("Alipay payout not implemented");
|
throw new UnsupportedOperationException("Alipay payout not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildPayeeSnapshot(ClerkPayeeProfileEntity profile, LocalDateTime confirmedAt) {
|
||||||
|
PayeeSnapshotVo snapshot = new PayeeSnapshotVo();
|
||||||
|
snapshot.setChannel(profile.getChannel());
|
||||||
|
snapshot.setQrCodeUrl(profile.getQrCodeUrl());
|
||||||
|
snapshot.setDisplayName(profile.getDisplayName());
|
||||||
|
snapshot.setConfirmedAt(confirmedAt);
|
||||||
|
return JSON.toJSONString(snapshot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.vo;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BackfillOrderPreview {
|
||||||
|
private String orderId;
|
||||||
|
private String clerkId;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private LocalDateTime orderEndTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.vo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class EarningsBackfillRequest {
|
||||||
|
private String beginTime;
|
||||||
|
private String endTime;
|
||||||
|
private List<String> clerkIds;
|
||||||
|
private Boolean dryRun;
|
||||||
|
private String comment;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.vo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class EarningsBackfillResponse {
|
||||||
|
private boolean dryRun;
|
||||||
|
private int ordersChecked;
|
||||||
|
private int createdCount;
|
||||||
|
private int skippedExisting;
|
||||||
|
private List<String> warnings;
|
||||||
|
private String logId;
|
||||||
|
private List<BackfillOrderPreview> orders;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.vo;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PayeeSnapshotVo {
|
||||||
|
private String channel;
|
||||||
|
private String qrCodeUrl;
|
||||||
|
private String displayName;
|
||||||
|
private LocalDateTime confirmedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Remove duplicate earnings lines per tenant/order/clerk/type and add unique constraint
|
||||||
|
|
||||||
|
DELETE FROM `play_earnings_line`
|
||||||
|
WHERE `id` IN (
|
||||||
|
SELECT dup_id FROM (
|
||||||
|
SELECT id AS dup_id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY tenant_id, order_id, clerk_id, earning_type
|
||||||
|
ORDER BY created_time, id
|
||||||
|
) AS rn
|
||||||
|
FROM `play_earnings_line`
|
||||||
|
WHERE deleted = 0
|
||||||
|
) ranked
|
||||||
|
WHERE ranked.rn > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE `play_earnings_line`
|
||||||
|
ADD UNIQUE KEY `uk_tenant_order_clerk_type` (`tenant_id`, `order_id`, `clerk_id`, `earning_type`, `deleted`);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- Clerk payee QR profile and withdrawal snapshot
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `play_clerk_payee_profile` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT 'UUID',
|
||||||
|
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID',
|
||||||
|
`clerk_id` varchar(32) NOT NULL COMMENT '店员ID',
|
||||||
|
`channel` varchar(32) NOT NULL DEFAULT 'ALIPAY_QR' COMMENT '收款渠道(例如:ALIPAY_QR)',
|
||||||
|
`qr_code_url` varchar(512) NOT NULL COMMENT '收款二维码地址',
|
||||||
|
`display_name` varchar(64) DEFAULT NULL COMMENT '收款码显示名称',
|
||||||
|
`last_confirmed_at` datetime DEFAULT NULL COMMENT '最近确认时间',
|
||||||
|
`created_by` varchar(32) DEFAULT NULL,
|
||||||
|
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_by` varchar(32) DEFAULT NULL,
|
||||||
|
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除 1已删除 0未删除',
|
||||||
|
`version` int NOT NULL DEFAULT '1' COMMENT '数据版本',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `uk_clerk_payee` (`tenant_id`, `clerk_id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='店员提现收款码档案';
|
||||||
|
|
||||||
|
ALTER TABLE `play_withdrawal_request`
|
||||||
|
ADD COLUMN `payee_snapshot` text COMMENT '提现时的收款码快照(JSON)';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Ensure each active tenant has a default freeze policy (7 days)
|
||||||
|
|
||||||
|
INSERT INTO `play_freeze_policy` (`id`, `tenant_id`, `clerk_id`, `freeze_hours`, `created_by`, `created_time`, `updated_by`, `updated_time`, `deleted`, `version`)
|
||||||
|
SELECT
|
||||||
|
REPLACE(UUID(), '-', ''),
|
||||||
|
t.`tenant_id`,
|
||||||
|
NULL,
|
||||||
|
168,
|
||||||
|
'migration_v8_default_freeze',
|
||||||
|
NOW(),
|
||||||
|
'migration_v8_default_freeze',
|
||||||
|
NOW(),
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE (t.`deleted` IS NULL OR t.`deleted` = 0)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM `play_freeze_policy` p
|
||||||
|
WHERE p.`tenant_id` = t.`tenant_id`
|
||||||
|
AND p.`clerk_id` IS NULL
|
||||||
|
AND (p.`deleted` IS NULL OR p.`deleted` = 0)
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- Audit log for earnings backfill executions
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `play_earnings_backfill_log` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT 'UUID',
|
||||||
|
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID',
|
||||||
|
`operator_id` varchar(32) DEFAULT NULL COMMENT '操作人ID',
|
||||||
|
`operator_name` varchar(64) DEFAULT NULL COMMENT '操作人名称',
|
||||||
|
`backfill_begin_time` datetime DEFAULT NULL COMMENT '查询起始时间',
|
||||||
|
`backfill_end_time` datetime DEFAULT NULL COMMENT '查询结束时间',
|
||||||
|
`clerk_ids` text COMMENT '指定店员ID集合(JSON)',
|
||||||
|
`dry_run` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否试运行',
|
||||||
|
`orders_checked` int NOT NULL DEFAULT '0' COMMENT '检查订单数',
|
||||||
|
`created_count` int NOT NULL DEFAULT '0' COMMENT '新增收益条目数',
|
||||||
|
`skipped_existing` int NOT NULL DEFAULT '0' COMMENT '跳过已存在收益条目数',
|
||||||
|
`warning_count` int NOT NULL DEFAULT '0' COMMENT '警告数量',
|
||||||
|
`warnings` text COMMENT '警告明细(JSON)',
|
||||||
|
`comment` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`created_by` varchar(32) DEFAULT NULL,
|
||||||
|
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`updated_by` varchar(32) DEFAULT NULL,
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除 1已删除 0未删除',
|
||||||
|
`version` int NOT NULL DEFAULT '1' COMMENT '数据版本',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_backfill_tenant_time` (`tenant_id`, `created_time`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='收益补算执行记录';
|
||||||
|
|
||||||
Reference in New Issue
Block a user