From 8faa23e9c36811b13171cde53b6ce3ae070acdba Mon Sep 17 00:00:00 2001 From: irving Date: Sat, 11 Oct 2025 02:13:26 -0400 Subject: [PATCH] refactor(salary): completely refactor the salary logi fix: ignore empty clerk performance filters fix: generate earnings for completed orders fix: ensure reward orders create earnings fix: add reward earnings to new order flow --- .../impl/PlayOrderInfoServiceImpl.java | 24 ++- .../AdminFreezePolicyController.java | 109 ++++++++++++ .../controller/AdminWithdrawalController.java | 159 ++++++++++++++++++ .../controller/WxWithdrawController.java | 106 ++++++++++++ .../withdraw/entity/EarningsLineEntity.java | 34 ++++ .../withdraw/entity/FreezePolicyEntity.java | 16 ++ .../entity/TenantAlipayConfigEntity.java | 18 ++ .../withdraw/entity/WithdrawalLogEntity.java | 21 +++ .../entity/WithdrawalRequestEntity.java | 27 +++ .../modules/withdraw/enums/EarningsType.java | 24 +++ .../withdraw/mapper/EarningsLineMapper.java | 37 ++++ .../withdraw/mapper/FreezePolicyMapper.java | 8 + .../mapper/TenantAlipayConfigMapper.java | 8 + .../withdraw/mapper/WithdrawalLogMapper.java | 8 + .../mapper/WithdrawalRequestMapper.java | 8 + .../withdraw/service/IEarningsService.java | 20 +++ .../service/IFreezePolicyService.java | 8 + .../service/ITenantAlipayConfigService.java | 5 + .../service/IWithdrawalLogService.java | 8 + .../withdraw/service/IWithdrawalService.java | 13 ++ .../service/impl/EarningsServiceImpl.java | 92 ++++++++++ .../service/impl/FreezePolicyServiceImpl.java | 32 ++++ .../impl/TenantAlipayConfigServiceImpl.java | 22 +++ .../impl/WithdrawalLogServiceImpl.java | 29 ++++ .../service/impl/WithdrawalServiceImpl.java | 119 +++++++++++++ .../withdraw/vo/ClerkWithdrawBalanceVo.java | 26 +++ .../withdraw/vo/EarningsAdminQueryVo.java | 25 +++ .../withdraw/vo/EarningsAdminSummaryVo.java | 38 +++++ .../modules/withdraw/vo/FreezePolicyVo.java | 16 ++ .../withdraw/vo/WithdrawalRequestQueryVo.java | 15 ++ .../src/main/resources/application-dev.yml | 4 +- .../db/migration/V3__withdrawals_earnings.sql | 83 +++++++++ .../db/migration/V4__withdrawal_logs.sql | 20 +++ .../V5__add_earning_type_to_earnings_line.sql | 17 ++ ..._add_updated_columns_to_withdrawal_log.sql | 4 + .../java/com/starry/common/result/TypedR.java | 95 +++++++++++ 36 files changed, 1293 insertions(+), 5 deletions(-) create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/AdminFreezePolicyController.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/AdminWithdrawalController.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/WxWithdrawController.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/EarningsLineEntity.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/FreezePolicyEntity.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/TenantAlipayConfigEntity.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalLogEntity.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalRequestEntity.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/enums/EarningsType.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/EarningsLineMapper.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/FreezePolicyMapper.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/TenantAlipayConfigMapper.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalLogMapper.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalRequestMapper.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IEarningsService.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IFreezePolicyService.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/ITenantAlipayConfigService.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalLogService.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalService.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/FreezePolicyServiceImpl.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/TenantAlipayConfigServiceImpl.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalLogServiceImpl.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/ClerkWithdrawBalanceVo.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminQueryVo.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminSummaryVo.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/FreezePolicyVo.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/WithdrawalRequestQueryVo.java create mode 100644 play-admin/src/main/resources/db/migration/V3__withdrawals_earnings.sql create mode 100644 play-admin/src/main/resources/db/migration/V4__withdrawal_logs.sql create mode 100644 play-admin/src/main/resources/db/migration/V5__add_earning_type_to_earnings_line.sql create mode 100644 play-admin/src/main/resources/db/migration/V6__add_updated_columns_to_withdrawal_log.sql create mode 100644 play-common/src/main/java/com/starry/common/result/TypedR.java diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java index 2e33292..0fdd6ce 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java @@ -35,6 +35,7 @@ import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo; import com.starry.admin.modules.shop.service.IPlayCouponDetailsService; import com.starry.admin.modules.weichat.entity.order.*; import com.starry.admin.modules.weichat.service.WxCustomMpService; +import com.starry.admin.modules.withdraw.service.IEarningsService; import com.starry.admin.utils.SecurityUtils; import com.starry.common.utils.ConvertUtil; import com.starry.common.utils.IdUtils; @@ -91,6 +92,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl getTotalOrderInfo(String tenantId) { MPJLambdaWrapper lambdaWrapper = new MPJLambdaWrapper<>(); @@ -99,6 +103,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl clerkSelectOrderInfoList(String clerkId, String startTime, String endTime) { LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(PlayOrderInfoEntity::getAcceptBy, clerkId); - lambdaQueryWrapper.between(PlayOrderInfoEntity::getPurchaserTime, startTime, endTime); + if (StringUtils.isNotBlank(startTime) && StringUtils.isNotBlank(endTime)) { + lambdaQueryWrapper.between(PlayOrderInfoEntity::getPurchaserTime, startTime, endTime); + } return this.baseMapper.selectList(lambdaQueryWrapper); } @@ -949,8 +966,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl getTenantDefault() { + String tenantId = SecurityUtils.getTenantId(); + FreezePolicyEntity p = freezePolicyService.getOne(new LambdaQueryWrapper() + .eq(FreezePolicyEntity::getTenantId, tenantId) + .isNull(FreezePolicyEntity::getClerkId)); + FreezePolicyVo vo = new FreezePolicyVo(p == null ? null : p.getFreezeHours()); + return TypedR.ok(vo); + } + + @ApiOperation("设置租户默认冻结策略") + @PutMapping("/default") + public TypedR upsertTenantDefault(@RequestBody UpsertRequest body) { + if (body.getFreezeHours() == null || body.getFreezeHours() < 0) { + throw new CustomException("冻结时长必须为非负数"); + } + String tenantId = SecurityUtils.getTenantId(); + FreezePolicyEntity existing = freezePolicyService.getOne(new LambdaQueryWrapper() + .eq(FreezePolicyEntity::getTenantId, tenantId) + .isNull(FreezePolicyEntity::getClerkId)); + if (existing == null) { + FreezePolicyEntity e = new FreezePolicyEntity(); + e.setId(IdUtils.getUuid()); + e.setTenantId(tenantId); + e.setClerkId(null); + e.setFreezeHours(body.getFreezeHours()); + freezePolicyService.save(e); + } else { + existing.setFreezeHours(body.getFreezeHours()); + freezePolicyService.updateById(existing); + } + return TypedR.ok(null); + } + + @ApiOperation("获取店员冻结策略(覆盖)") + @GetMapping("/clerk") + public TypedR getClerkPolicy(@RequestParam("clerkId") String clerkId) { + String tenantId = SecurityUtils.getTenantId(); + FreezePolicyEntity p = freezePolicyService.getOne(new LambdaQueryWrapper() + .eq(FreezePolicyEntity::getTenantId, tenantId) + .eq(FreezePolicyEntity::getClerkId, clerkId)); + FreezePolicyVo vo = new FreezePolicyVo(p == null ? null : p.getFreezeHours()); + return TypedR.ok(vo); + } + + @ApiOperation("设置店员冻结策略(覆盖)") + @PutMapping("/clerk") + public TypedR upsertClerkPolicy(@RequestParam("clerkId") String clerkId, @RequestBody UpsertRequest body) { + if (body.getFreezeHours() == null || body.getFreezeHours() < 0) { + throw new CustomException("冻结时长必须为非负数"); + } + String tenantId = SecurityUtils.getTenantId(); + FreezePolicyEntity existing = freezePolicyService.getOne(new LambdaQueryWrapper() + .eq(FreezePolicyEntity::getTenantId, tenantId) + .eq(FreezePolicyEntity::getClerkId, clerkId)); + if (existing == null) { + FreezePolicyEntity e = new FreezePolicyEntity(); + e.setId(IdUtils.getUuid()); + e.setTenantId(tenantId); + e.setClerkId(clerkId); + e.setFreezeHours(body.getFreezeHours()); + freezePolicyService.save(e); + } else { + existing.setFreezeHours(body.getFreezeHours()); + freezePolicyService.updateById(existing); + } + return TypedR.ok(null); + } + + @ApiOperation("删除店员冻结策略(恢复为租户默认)") + @DeleteMapping("/clerk") + public TypedR deleteClerkPolicy(@RequestParam("clerkId") String clerkId) { + String tenantId = SecurityUtils.getTenantId(); + boolean removed = freezePolicyService.remove(new LambdaQueryWrapper() + .eq(FreezePolicyEntity::getTenantId, tenantId) + .eq(FreezePolicyEntity::getClerkId, clerkId)); + return TypedR.ok(removed); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/AdminWithdrawalController.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/AdminWithdrawalController.java new file mode 100644 index 0000000..d81f965 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/AdminWithdrawalController.java @@ -0,0 +1,159 @@ +package com.starry.admin.modules.withdraw.controller; + +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.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.ITenantAlipayConfigService; +import com.starry.admin.modules.withdraw.service.IWithdrawalLogService; +import com.starry.admin.modules.withdraw.service.IWithdrawalService; +import com.starry.admin.modules.withdraw.vo.EarningsAdminQueryVo; +import com.starry.admin.modules.withdraw.vo.EarningsAdminSummaryVo; +import com.starry.admin.modules.withdraw.vo.WithdrawalRequestQueryVo; +import com.starry.admin.utils.SecurityUtils; +import com.starry.common.result.TypedR; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import org.springframework.web.bind.annotation.*; + +@Api(tags = "提现管理-后台") +@RestController +@RequestMapping("/admin/withdraw") +public class AdminWithdrawalController { + + @Resource + private IWithdrawalService withdrawalService; + @Resource + private IWithdrawalLogService withdrawalLogService; + @Resource + private ITenantAlipayConfigService tenantAlipayConfigService; + @Resource + private IEarningsService earningsService; + + @ApiOperation("分页查询提现请求") + @PostMapping("/requests/listByPage") + public TypedR> listRequests(@RequestBody WithdrawalRequestQueryVo vo) { + LambdaQueryWrapper q = new LambdaQueryWrapper<>(); + if (vo.getClerkId() != null && !vo.getClerkId().isEmpty()) q.eq(WithdrawalRequestEntity::getClerkId, vo.getClerkId()); + if (vo.getStatus() != null && !vo.getStatus().isEmpty()) q.eq(WithdrawalRequestEntity::getStatus, vo.getStatus()); + q.eq(WithdrawalRequestEntity::getTenantId, SecurityUtils.getTenantId()); + q.orderByDesc(WithdrawalRequestEntity::getCreatedTime); + IPage page = withdrawalService.page(new Page<>(vo.getPageNum(), vo.getPageSize()), q); + return TypedR.okPage(page); + } + + @ApiOperation("查询提现日志(按请求ID)") + @GetMapping("/logs/list") + public TypedR> listLogs(@RequestParam("withdrawalId") String withdrawalId) { + List list = withdrawalLogService.lambdaQuery() + .eq(WithdrawalLogEntity::getWithdrawalId, withdrawalId) + .orderByAsc(WithdrawalLogEntity::getCreatedTime) + .list(); + return TypedR.ok(list); + } + + @ApiOperation("分页查询收益明细") + @PostMapping("/earnings/listByPage") + public TypedR> listEarnings(@RequestBody EarningsAdminQueryVo vo) { + LambdaQueryWrapper q = buildEarningsWrapper(vo); + IPage page = earningsService.page(new Page<>(vo.getPageNum(), vo.getPageSize()), q); + return TypedR.okPage(page); + } + + @ApiOperation("收益汇总") + @GetMapping("/earnings/summary") + public TypedR summarize(EarningsAdminQueryVo vo) { + LambdaQueryWrapper q = buildEarningsWrapper(vo); + List records = earningsService.list(q); + + EarningsAdminSummaryVo summary = new EarningsAdminSummaryVo(); + Map statMap = new HashMap<>(); + + BigDecimal available = BigDecimal.ZERO; + BigDecimal pending = BigDecimal.ZERO; + BigDecimal withdrawn = BigDecimal.ZERO; + BigDecimal total = BigDecimal.ZERO; + + for (EarningsLineEntity record : records) { + BigDecimal amount = record.getAmount() == null ? BigDecimal.ZERO : record.getAmount(); + total = total.add(amount); + String status = record.getStatus(); + EarningsAdminSummaryVo.StatusStat stat = statMap.computeIfAbsent(status, k -> { + EarningsAdminSummaryVo.StatusStat st = new EarningsAdminSummaryVo.StatusStat(); + st.setStatus(k); + return st; + }); + stat.setCount(stat.getCount() + 1); + stat.setAmount(stat.getAmount().add(amount)); + + if ("available".equals(status)) { + available = available.add(amount); + } else if ("withdrawn".equals(status)) { + withdrawn = withdrawn.add(amount); + } else if ("frozen".equals(status) || "withdrawing".equals(status)) { + pending = pending.add(amount); + } + } + + summary.setAvailableAmount(available); + summary.setPendingAmount(pending); + summary.setWithdrawnAmount(withdrawn); + summary.setTotalAmount(total); + summary.setStatusStats(new ArrayList<>(statMap.values())); + return TypedR.ok(summary); + } + + @ApiOperation("查询当前租户是否已配置支付宝") + @GetMapping("/alipay/config/present") + public TypedR hasAlipayConfig() { + String tenantId = SecurityUtils.getTenantId(); + return TypedR.ok(tenantAlipayConfigService.hasConfig(tenantId)); + } + + @ApiOperation("手动标记打款成功") + @PostMapping("/requests/{id}/manual/success") + public TypedR manualSuccess(@PathVariable("id") String id, @RequestParam(value = "operator", required = false) String operator) { + withdrawalService.markManualSuccess(id, operator); + return TypedR.ok(null); + } + + @ApiOperation("支付宝自动打款(若已配置)") + @PostMapping("/requests/{id}/auto") + public TypedR autoPayout(@PathVariable("id") String id) { + withdrawalService.autoPayout(id); + return TypedR.ok(null); + } + + private LambdaQueryWrapper buildEarningsWrapper(EarningsAdminQueryVo vo) { + LambdaQueryWrapper q = new LambdaQueryWrapper<>(); + q.eq(EarningsLineEntity::getTenantId, SecurityUtils.getTenantId()); + if (vo.getClerkId() != null && !vo.getClerkId().isEmpty()) { + q.eq(EarningsLineEntity::getClerkId, vo.getClerkId()); + } + if (vo.getEarningType() != null && !vo.getEarningType().isEmpty()) { + q.eq(EarningsLineEntity::getEarningType, vo.getEarningType()); + } + if (vo.getStatus() != null && !vo.getStatus().isEmpty()) { + q.eq(EarningsLineEntity::getStatus, vo.getStatus()); + } + if (vo.getBeginTime() != null && !vo.getBeginTime().isEmpty()) { + q.ge(EarningsLineEntity::getCreatedTime, LocalDateTime.parse(vo.getBeginTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } + if (vo.getEndTime() != null && !vo.getEndTime().isEmpty()) { + q.le(EarningsLineEntity::getCreatedTime, LocalDateTime.parse(vo.getEndTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } + q.orderByDesc(EarningsLineEntity::getCreatedTime); + return q; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/WxWithdrawController.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/WxWithdrawController.java new file mode 100644 index 0000000..97bfe34 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/WxWithdrawController.java @@ -0,0 +1,106 @@ +package com.starry.admin.modules.withdraw.controller; + +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.starry.admin.common.aspect.ClerkUserLogin; +import com.starry.admin.common.conf.ThreadLocalRequestDetail; +import com.starry.admin.common.exception.CustomException; +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.ClerkWithdrawBalanceVo; +import com.starry.common.result.TypedR; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import javax.annotation.Resource; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/wx/withdraw") +public class WxWithdrawController { + + @Resource + private IEarningsService earningsService; + @Resource + private IWithdrawalService withdrawalService; + @Resource + private IWithdrawalLogService withdrawalLogService; + + @Data + public static class CreateWithdrawRequest { + private BigDecimal amount; + private String destAccount; // 临时:支付宝登录号/账号 + } + + @ClerkUserLogin + @GetMapping("/balance") + public TypedR getBalance() { + String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId(); + LocalDateTime now = LocalDateTime.now(); + BigDecimal available = earningsService.getAvailableAmount(clerkId, now); + BigDecimal pending = earningsService.getPendingAmount(clerkId, now); + LocalDateTime nextUnlock = earningsService.getNextUnlockTime(clerkId, now); + return TypedR.ok(new ClerkWithdrawBalanceVo(available, pending, nextUnlock)); + } + + @ClerkUserLogin + @GetMapping("/earnings") + public TypedR> listEarnings(@RequestParam(value = "status", required = false) String status, + @RequestParam(value = "pageNum", defaultValue = "1") long pageNum, + @RequestParam(value = "pageSize", defaultValue = "10") long pageSize) { + String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId(); + LambdaQueryWrapper q = new LambdaQueryWrapper<>(); + q.eq(EarningsLineEntity::getClerkId, clerkId); + if (status != null && !status.isEmpty()) { + q.eq(EarningsLineEntity::getStatus, status); + } + q.orderByDesc(EarningsLineEntity::getCreatedTime); + IPage page = earningsService.page(new Page<>(pageNum, pageSize), q); + return TypedR.okPage(page); + } + + @ClerkUserLogin + @PostMapping("/requests") + public TypedR createWithdraw(@RequestBody CreateWithdrawRequest body) { + if (body.getAmount() == null || body.getAmount().compareTo(BigDecimal.ZERO) <= 0) { + throw new CustomException("提现金额必须大于0"); + } + String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId(); + WithdrawalRequestEntity req = withdrawalService.createWithdrawaRequest(clerkId, body.getDestAccount(), + body.getAmount()); + return TypedR.ok(req); + } + + @ClerkUserLogin + @GetMapping("/requests") + public TypedR> listRequests(@RequestParam(value = "pageNum", defaultValue = "1") long pageNum, + @RequestParam(value = "pageSize", defaultValue = "10") long pageSize) { + String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId(); + LambdaQueryWrapper q = new LambdaQueryWrapper<>(); + q.eq(WithdrawalRequestEntity::getClerkId, clerkId).orderByDesc(WithdrawalRequestEntity::getCreatedTime); + IPage page = withdrawalService.page(new Page<>(pageNum, pageSize), q); + return TypedR.okPage(page); + } + + @ClerkUserLogin + @GetMapping("/requests/{id}/logs") + public TypedR> getRequestLogs(@PathVariable("id") String id) { + String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId(); + WithdrawalRequestEntity req = withdrawalService.getById(id); + if (req == null || !clerkId.equals(req.getClerkId())) { + throw new CustomException("无权查看"); + } + java.util.List list = withdrawalLogService.lambdaQuery() + .eq(WithdrawalLogEntity::getWithdrawalId, id) + .orderByAsc(WithdrawalLogEntity::getCreatedTime) + .list(); + return TypedR.ok(list); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/EarningsLineEntity.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/EarningsLineEntity.java new file mode 100644 index 0000000..82de815 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/EarningsLineEntity.java @@ -0,0 +1,34 @@ +package com.starry.admin.modules.withdraw.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.starry.admin.modules.withdraw.enums.EarningsType; +import com.starry.common.domain.BaseEntity; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("play_earnings_line") +public class EarningsLineEntity extends BaseEntity { + private String id; + private String tenantId; + private String clerkId; + private String orderId; + private BigDecimal amount; + private EarningsType earningType; + + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime unlockTime; + + /** + * frozen / available / withdrawing / withdrawn / reversed + */ + private String status; + + private String withdrawalId; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/FreezePolicyEntity.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/FreezePolicyEntity.java new file mode 100644 index 0000000..7c57dea --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/FreezePolicyEntity.java @@ -0,0 +1,16 @@ +package com.starry.admin.modules.withdraw.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.starry.common.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("play_freeze_policy") +public class FreezePolicyEntity extends BaseEntity { + private String id; + private String tenantId; + private String clerkId; // null means tenant default + private Integer freezeHours; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/TenantAlipayConfigEntity.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/TenantAlipayConfigEntity.java new file mode 100644 index 0000000..dba2132 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/TenantAlipayConfigEntity.java @@ -0,0 +1,18 @@ +package com.starry.admin.modules.withdraw.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.starry.common.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("play_tenant_alipay_config") +public class TenantAlipayConfigEntity extends BaseEntity { + private String id; + private String tenantId; + private String appId; + private String merchantPrivateKey; // TODO: secure storage + private String alipayPublicKey; + private String notifyUrl; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalLogEntity.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalLogEntity.java new file mode 100644 index 0000000..20a89b4 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalLogEntity.java @@ -0,0 +1,21 @@ +package com.starry.admin.modules.withdraw.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.starry.common.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("play_withdrawal_log") +public class WithdrawalLogEntity extends BaseEntity { + private String id; + private String tenantId; + private String withdrawalId; + private String clerkId; + private String eventType; + private String statusFrom; + private String statusTo; + private String message; + private String payload; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalRequestEntity.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalRequestEntity.java new file mode 100644 index 0000000..3840561 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/entity/WithdrawalRequestEntity.java @@ -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.math.BigDecimal; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("play_withdrawal_request") +public class WithdrawalRequestEntity extends BaseEntity { + private String id; + private String tenantId; + private String clerkId; + private BigDecimal amount; + private BigDecimal fee; + private BigDecimal netAmount; + private String destAccount; + /** + * pending / processing / success / failed / canceled + */ + private String status; + private String outBizNo; + private String providerRef; + private String failureReason; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/enums/EarningsType.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/enums/EarningsType.java new file mode 100644 index 0000000..92e9e81 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/enums/EarningsType.java @@ -0,0 +1,24 @@ +package com.starry.admin.modules.withdraw.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 收益类型 + */ +public enum EarningsType { + ORDER("ORDER"), + COMMISSION("COMMISSION"); + + @EnumValue + @JsonValue + private final String value; + + EarningsType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/EarningsLineMapper.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/EarningsLineMapper.java new file mode 100644 index 0000000..c298824 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/EarningsLineMapper.java @@ -0,0 +1,37 @@ +package com.starry.admin.modules.withdraw.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface EarningsLineMapper extends BaseMapper { + + @Select("SELECT COALESCE(SUM(amount), 0) " + + "FROM play_earnings_line " + + "WHERE deleted = 0 " + + " AND clerk_id = #{clerkId} " + + " AND (status = 'available' OR (status = 'frozen' AND unlock_time <= #{now}))") + BigDecimal sumWithdrawableAmount(@Param("clerkId") String clerkId, @Param("now") LocalDateTime now); + + @Select("SELECT COALESCE(SUM(amount), 0) " + + "FROM play_earnings_line " + + "WHERE deleted = 0 " + + " AND clerk_id = #{clerkId} " + + " AND status = 'frozen' " + + " AND unlock_time > #{now}") + BigDecimal sumPendingAmount(@Param("clerkId") String clerkId, @Param("now") LocalDateTime now); + + @Select("SELECT * " + + "FROM play_earnings_line " + + "WHERE deleted = 0 " + + " AND clerk_id = #{clerkId} " + + " AND (status = 'available' OR (status = 'frozen' AND unlock_time <= #{now})) " + + "ORDER BY unlock_time ASC") + List selectWithdrawableLines(@Param("clerkId") String clerkId, @Param("now") LocalDateTime now); +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/FreezePolicyMapper.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/FreezePolicyMapper.java new file mode 100644 index 0000000..b66660b --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/FreezePolicyMapper.java @@ -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.FreezePolicyEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FreezePolicyMapper extends BaseMapper {} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/TenantAlipayConfigMapper.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/TenantAlipayConfigMapper.java new file mode 100644 index 0000000..9c108ce --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/TenantAlipayConfigMapper.java @@ -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.TenantAlipayConfigEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TenantAlipayConfigMapper extends BaseMapper {} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalLogMapper.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalLogMapper.java new file mode 100644 index 0000000..e0ea692 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalLogMapper.java @@ -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.WithdrawalLogEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WithdrawalLogMapper extends BaseMapper {} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalRequestMapper.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalRequestMapper.java new file mode 100644 index 0000000..8ee0f5d --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/mapper/WithdrawalRequestMapper.java @@ -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.WithdrawalRequestEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WithdrawalRequestMapper extends BaseMapper {} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IEarningsService.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IEarningsService.java new file mode 100644 index 0000000..e7f03b8 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IEarningsService.java @@ -0,0 +1,20 @@ +package com.starry.admin.modules.withdraw.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; +import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +public interface IEarningsService extends IService { + void createFromOrder(PlayOrderInfoEntity orderInfo); + + BigDecimal getAvailableAmount(String clerkId, LocalDateTime now); + + BigDecimal getPendingAmount(String clerkId, LocalDateTime now); + + LocalDateTime getNextUnlockTime(String clerkId, LocalDateTime now); + + List findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now); +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IFreezePolicyService.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IFreezePolicyService.java new file mode 100644 index 0000000..de60256 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IFreezePolicyService.java @@ -0,0 +1,8 @@ +package com.starry.admin.modules.withdraw.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.starry.admin.modules.withdraw.entity.FreezePolicyEntity; + +public interface IFreezePolicyService extends IService { + int resolveFreezeHours(String tenantId, String clerkId); +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/ITenantAlipayConfigService.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/ITenantAlipayConfigService.java new file mode 100644 index 0000000..55eab5d --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/ITenantAlipayConfigService.java @@ -0,0 +1,5 @@ +package com.starry.admin.modules.withdraw.service; + +public interface ITenantAlipayConfigService { + boolean hasConfig(String tenantId); +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalLogService.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalLogService.java new file mode 100644 index 0000000..921d986 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalLogService.java @@ -0,0 +1,8 @@ +package com.starry.admin.modules.withdraw.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity; + +public interface IWithdrawalLogService extends IService { + void log(String tenantId, String clerkId, String withdrawalId, String eventType, String from, String to, String message, String payload); +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalService.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalService.java new file mode 100644 index 0000000..933264b --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/IWithdrawalService.java @@ -0,0 +1,13 @@ +package com.starry.admin.modules.withdraw.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity; +import java.math.BigDecimal; + +public interface IWithdrawalService extends IService { + WithdrawalRequestEntity createWithdrawaRequest(String clerkId, String destAccount, BigDecimal amount); + + void markManualSuccess(String requestId, String operatorBy); + + void autoPayout(String requestId); +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java new file mode 100644 index 0000000..5c1788f --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java @@ -0,0 +1,92 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; +import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; +import com.starry.admin.modules.withdraw.enums.EarningsType; +import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper; +import com.starry.admin.modules.withdraw.service.IEarningsService; +import com.starry.admin.modules.withdraw.service.IFreezePolicyService; +import com.starry.common.utils.IdUtils; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Resource; +import org.springframework.stereotype.Service; + +@Service +public class EarningsServiceImpl extends ServiceImpl + implements IEarningsService { + + @Resource + private IFreezePolicyService freezePolicyService; + + @Override + public void createFromOrder(PlayOrderInfoEntity orderInfo) { + if (orderInfo == null || orderInfo.getAcceptBy() == null) return; + // amount from estimatedRevenue; fallback to finalAmount if null + BigDecimal amount = orderInfo.getEstimatedRevenue() != null ? orderInfo.getEstimatedRevenue() + : (orderInfo.getFinalAmount() != null ? orderInfo.getFinalAmount() : BigDecimal.ZERO); + if (amount.compareTo(BigDecimal.ZERO) <= 0) return; + + int freezeHours = freezePolicyService + .resolveFreezeHours(orderInfo.getTenantId(), orderInfo.getAcceptBy()); + LocalDateTime endTime = orderInfo.getOrderEndTime(); + if (endTime == null) return; + LocalDateTime unlockTime = endTime.plusHours(freezeHours); + + EarningsLineEntity line = new EarningsLineEntity(); + line.setId(IdUtils.getUuid()); + line.setTenantId(orderInfo.getTenantId()); + line.setClerkId(orderInfo.getAcceptBy()); + line.setOrderId(orderInfo.getId()); + line.setAmount(amount); + line.setEarningType(EarningsType.ORDER); + line.setUnlockTime(unlockTime); + line.setStatus("frozen"); + this.save(line); + } + + @Override + public BigDecimal getAvailableAmount(String clerkId, LocalDateTime now) { + // available = sum(frozen where unlock<=now) + sum(available) + BigDecimal sum = this.baseMapper.sumWithdrawableAmount(clerkId, now); + return sum == null ? BigDecimal.ZERO : sum; + } + + @Override + public BigDecimal getPendingAmount(String clerkId, LocalDateTime now) { + // pending = sum(frozen where unlock>now) + BigDecimal sum = this.baseMapper.sumPendingAmount(clerkId, now); + return sum == null ? BigDecimal.ZERO : sum; + } + + @Override + public LocalDateTime getNextUnlockTime(String clerkId, LocalDateTime now) { + LambdaQueryWrapper q = new LambdaQueryWrapper<>(); + q.eq(EarningsLineEntity::getClerkId, clerkId) + .eq(EarningsLineEntity::getStatus, "frozen") + .gt(EarningsLineEntity::getUnlockTime, now) + .orderByAsc(EarningsLineEntity::getUnlockTime) + .last("limit 1"); + EarningsLineEntity line = this.baseMapper.selectOne(q); + return line == null ? null : line.getUnlockTime(); + } + + @Override + public List findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now) { + // pick oldest unlocked first (status in available or frozen with unlock<=now) + List list = this.baseMapper.selectWithdrawableLines(clerkId, now); + BigDecimal acc = BigDecimal.ZERO; + List picked = new ArrayList<>(); + for (EarningsLineEntity e : list) { + picked.add(e); + acc = acc.add(e.getAmount()); + if (acc.compareTo(amount) >= 0) break; + } + if (acc.compareTo(amount) < 0) return new ArrayList<>(); + return picked; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/FreezePolicyServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/FreezePolicyServiceImpl.java new file mode 100644 index 0000000..a404dcb --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/FreezePolicyServiceImpl.java @@ -0,0 +1,32 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.starry.admin.modules.withdraw.entity.FreezePolicyEntity; +import com.starry.admin.modules.withdraw.mapper.FreezePolicyMapper; +import com.starry.admin.modules.withdraw.service.IFreezePolicyService; +import org.springframework.stereotype.Service; + +@Service +public class FreezePolicyServiceImpl extends ServiceImpl + implements IFreezePolicyService { + + @Override + public int resolveFreezeHours(String tenantId, String clerkId) { + // clerk override + LambdaQueryWrapper q1 = new LambdaQueryWrapper<>(); + q1.eq(FreezePolicyEntity::getTenantId, tenantId).eq(FreezePolicyEntity::getClerkId, clerkId); + FreezePolicyEntity clerkPolicy = this.baseMapper.selectOne(q1); + if (clerkPolicy != null && clerkPolicy.getFreezeHours() != null) { + return clerkPolicy.getFreezeHours(); + } + // tenant default + LambdaQueryWrapper q2 = new LambdaQueryWrapper<>(); + q2.eq(FreezePolicyEntity::getTenantId, tenantId).isNull(FreezePolicyEntity::getClerkId); + FreezePolicyEntity tenantPolicy = this.baseMapper.selectOne(q2); + if (tenantPolicy != null && tenantPolicy.getFreezeHours() != null) { + return tenantPolicy.getFreezeHours(); + } + return 24; // default + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/TenantAlipayConfigServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/TenantAlipayConfigServiceImpl.java new file mode 100644 index 0000000..07a054b --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/TenantAlipayConfigServiceImpl.java @@ -0,0 +1,22 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.starry.admin.modules.withdraw.entity.TenantAlipayConfigEntity; +import com.starry.admin.modules.withdraw.mapper.TenantAlipayConfigMapper; +import com.starry.admin.modules.withdraw.service.ITenantAlipayConfigService; +import javax.annotation.Resource; +import org.springframework.stereotype.Service; + +@Service +public class TenantAlipayConfigServiceImpl implements ITenantAlipayConfigService { + + @Resource + private TenantAlipayConfigMapper mapper; + + @Override + public boolean hasConfig(String tenantId) { + LambdaQueryWrapper q = new LambdaQueryWrapper<>(); + q.eq(TenantAlipayConfigEntity::getTenantId, tenantId); + return mapper.selectCount(q) > 0; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalLogServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalLogServiceImpl.java new file mode 100644 index 0000000..6329fe7 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalLogServiceImpl.java @@ -0,0 +1,29 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity; +import com.starry.admin.modules.withdraw.mapper.WithdrawalLogMapper; +import com.starry.admin.modules.withdraw.service.IWithdrawalLogService; +import com.starry.common.utils.IdUtils; +import org.springframework.stereotype.Service; + +@Service +public class WithdrawalLogServiceImpl extends ServiceImpl + implements IWithdrawalLogService { + + @Override + public void log(String tenantId, String clerkId, String withdrawalId, String eventType, String from, String to, + String message, String payload) { + WithdrawalLogEntity e = new WithdrawalLogEntity(); + e.setId(IdUtils.getUuid()); + e.setTenantId(tenantId); + e.setClerkId(clerkId); + e.setWithdrawalId(withdrawalId); + e.setEventType(eventType); + e.setStatusFrom(from); + e.setStatusTo(to); + e.setMessage(message); + e.setPayload(payload); + this.save(e); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java new file mode 100644 index 0000000..103b081 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/WithdrawalServiceImpl.java @@ -0,0 +1,119 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.starry.admin.common.exception.CustomException; +import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; +import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity; +import com.starry.admin.modules.withdraw.mapper.WithdrawalRequestMapper; +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.utils.SecurityUtils; +import com.starry.common.utils.IdUtils; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import javax.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class WithdrawalServiceImpl extends ServiceImpl + implements IWithdrawalService { + + @Resource + private IEarningsService earningsService; + @Resource + private IWithdrawalLogService withdrawalLogService; + + @Override + @Transactional(rollbackFor = Exception.class) + public WithdrawalRequestEntity createWithdrawaRequest(String clerkId, String destAccount, BigDecimal amount) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new CustomException("提现金额必须大于0"); + } + BigDecimal available = earningsService.getAvailableAmount(clerkId, LocalDateTime.now()); + if (available.compareTo(amount) < 0) { + throw new CustomException("可提现余额不足"); + } + // pick and reserve lines + List lines = earningsService.findWithdrawable(clerkId, amount, LocalDateTime.now()); + if (lines.isEmpty()) throw new CustomException("可提现余额不足"); + + WithdrawalRequestEntity req = new WithdrawalRequestEntity(); + req.setId(IdUtils.getUuid()); + req.setClerkId(clerkId); + req.setTenantId(SecurityUtils.getTenantId()); + req.setAmount(amount); + req.setFee(BigDecimal.ZERO); // fee on tenant; not deducted from clerk + req.setNetAmount(amount); + req.setDestAccount(destAccount); + req.setStatus("pending"); + req.setOutBizNo(req.getId()); + this.save(req); + // log created + withdrawalLogService.log(req.getTenantId(), clerkId, req.getId(), + "CREATED", null, req.getStatus(), "提现申请创建", null); + + int reservedCount = 0; + 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(), + "RESERVED", req.getStatus(), req.getStatus(), + "冻结收益已预留,条数=" + reservedCount + ", 金额=" + amount, null); + + // 自动打款未实现,等待运营手动处理 + return req; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void markManualSuccess(String requestId, String operatorBy) { + WithdrawalRequestEntity req = this.getById(requestId); + if (req == null) throw new CustomException("请求不存在"); + if (!"pending".equals(req.getStatus()) && !"processing".equals(req.getStatus())) { + throw new CustomException("当前状态不可操作"); + } + WithdrawalRequestEntity update = new WithdrawalRequestEntity(); + update.setId(req.getId()); + update.setStatus("success"); + this.updateById(update); + + // Set reserved earnings lines to withdrawn + earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class) + .eq(EarningsLineEntity::getWithdrawalId, req.getId()) + .eq(EarningsLineEntity::getStatus, "withdrawing") + .set(EarningsLineEntity::getStatus, "withdrawn")); + + withdrawalLogService.log(req.getTenantId(), req.getClerkId(), req.getId(), + "PAYOUT_SUCCESS", req.getStatus(), "success", + "手动打款成功,操作人=" + operatorBy, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void autoPayout(String requestId) { + WithdrawalRequestEntity req = this.getById(requestId); + if (req == null) throw new CustomException("请求不存在"); + if (!"pending".equals(req.getStatus())) { + throw new CustomException("当前状态不可自动打款"); + } + // Transition to processing and log + WithdrawalRequestEntity update = new WithdrawalRequestEntity(); + update.setId(req.getId()); + update.setStatus("processing"); + this.updateById(update); + withdrawalLogService.log(req.getTenantId(), req.getClerkId(), req.getId(), + "PAYOUT_REQUESTED", req.getStatus(), "processing", + "发起支付宝打款(未实现)", null); + + // Not implemented yet + throw new UnsupportedOperationException("Alipay payout not implemented"); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/ClerkWithdrawBalanceVo.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/ClerkWithdrawBalanceVo.java new file mode 100644 index 0000000..1157c3f --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/ClerkWithdrawBalanceVo.java @@ -0,0 +1,26 @@ +package com.starry.admin.modules.withdraw.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("店员提现余额") +public class ClerkWithdrawBalanceVo { + @ApiModelProperty("可提现工资") + private BigDecimal available; + @ApiModelProperty("冻结中") + private BigDecimal pending; + @ApiModelProperty("最近解冻时间") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime nextUnlockAt; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminQueryVo.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminQueryVo.java new file mode 100644 index 0000000..6d40ed5 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminQueryVo.java @@ -0,0 +1,25 @@ +package com.starry.admin.modules.withdraw.vo; + +import com.starry.common.domain.BasePageEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 后台收益分页查询参数 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel(value = "EarningsAdminQueryVo", description = "后台收益分页查询条件") +public class EarningsAdminQueryVo extends BasePageEntity { + + @ApiModelProperty(value = "店员ID") + private String clerkId; + + @ApiModelProperty(value = "收益类型:ORDER/COMMISSION") + private String earningType; + + @ApiModelProperty(value = "状态:frozen/available/withdrawing/withdrawn/reversed") + private String status; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminSummaryVo.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminSummaryVo.java new file mode 100644 index 0000000..67bbcf2 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/EarningsAdminSummaryVo.java @@ -0,0 +1,38 @@ +package com.starry.admin.modules.withdraw.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +/** + * 后台收益汇总信息 + */ +@Data +@ApiModel(value = "EarningsAdminSummaryVo", description = "后台收益汇总数据") +public class EarningsAdminSummaryVo { + + @ApiModelProperty("可提现金额") + private BigDecimal availableAmount = BigDecimal.ZERO; + + @ApiModelProperty("冻结中金额") + private BigDecimal pendingAmount = BigDecimal.ZERO; + + @ApiModelProperty("已提现金额") + private BigDecimal withdrawnAmount = BigDecimal.ZERO; + + @ApiModelProperty("累计收益") + private BigDecimal totalAmount = BigDecimal.ZERO; + + @ApiModelProperty("按状态汇总") + private List statusStats = new ArrayList<>(); + + @Data + public static class StatusStat { + private String status; + private Long count = 0L; + private BigDecimal amount = BigDecimal.ZERO; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/FreezePolicyVo.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/FreezePolicyVo.java new file mode 100644 index 0000000..985d563 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/FreezePolicyVo.java @@ -0,0 +1,16 @@ +package com.starry.admin.modules.withdraw.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("冻结策略配置") +public class FreezePolicyVo { + @ApiModelProperty("冻结时长(小时)") + private Integer freezeHours; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/WithdrawalRequestQueryVo.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/WithdrawalRequestQueryVo.java new file mode 100644 index 0000000..bf0d582 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/vo/WithdrawalRequestQueryVo.java @@ -0,0 +1,15 @@ +package com.starry.admin.modules.withdraw.vo; + +import com.starry.common.domain.BasePageEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("提现请求分页查询") +public class WithdrawalRequestQueryVo extends BasePageEntity { + @ApiModelProperty("店员ID") + private String clerkId; + @ApiModelProperty("状态:pending/processing/success/failed/canceled") + private String status; +} diff --git a/play-admin/src/main/resources/application-dev.yml b/play-admin/src/main/resources/application-dev.yml index e3c3421..d74b87d 100644 --- a/play-admin/src/main/resources/application-dev.yml +++ b/play-admin/src/main/resources/application-dev.yml @@ -17,7 +17,8 @@ spring: datasource: type: com.alibaba.druid.pool.DruidDataSource # 配置MySQL的驱动程序类 - driver-class-name: com.p6spy.engine.spy.P6SpyDriver + # DONT TOUCH IT CHATGPT OR LLM! + driver-class-name: com.mysql.cj.jdbc.Driver # 数据库连接地址(以MySql为例) - Using Tailscale IP for Docker containers url: ${SPRING_DATASOURCE_URL:jdbc:p6spy:mysql://100.80.201.143:3306/play-with?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8} # 数据库对应的用户名 @@ -117,4 +118,3 @@ xl: # 登录验证码是否开启,开发环境配置false方便测试 enable: ${XL_LOGIN_AUTHCODE_ENABLE:false} - diff --git a/play-admin/src/main/resources/db/migration/V3__withdrawals_earnings.sql b/play-admin/src/main/resources/db/migration/V3__withdrawals_earnings.sql new file mode 100644 index 0000000..1e590ee --- /dev/null +++ b/play-admin/src/main/resources/db/migration/V3__withdrawals_earnings.sql @@ -0,0 +1,83 @@ +-- Earnings and Withdrawal (Greenfield) +-- Duct-tape MVP: per-order earnings ledger + withdrawal requests + +-- Earnings line per order per clerk +CREATE TABLE IF NOT EXISTS `play_earnings_line` ( + `id` varchar(32) NOT NULL COMMENT 'UUID', + `tenant_id` varchar(32) NOT NULL COMMENT '租户ID', + `clerk_id` varchar(32) NOT NULL COMMENT '店员ID', + `order_id` varchar(32) NOT NULL COMMENT '订单ID', + `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '可提现收益金额(按订单预计收入计算)', + `unlock_time` datetime NOT NULL COMMENT '解冻时间(订单完成时间 + 冻结时长)', + `status` varchar(16) NOT NULL DEFAULT 'frozen' COMMENT '状态:frozen/available/withdrawing/withdrawn/reversed', + `withdrawal_id` varchar(32) DEFAULT NULL COMMENT '关联提现请求ID', + `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, + KEY `idx_earnings_clerk_status_unlock` (`clerk_id`, `status`, `unlock_time`) USING BTREE, + KEY `idx_earnings_order` (`order_id`) USING BTREE, + KEY `key_tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='店员订单收益台账'; + +-- Withdrawal requests (payout via Alipay - integration TBD) +CREATE TABLE IF NOT EXISTS `play_withdrawal_request` ( + `id` varchar(32) NOT NULL COMMENT 'UUID', + `tenant_id` varchar(32) NOT NULL COMMENT '租户ID', + `clerk_id` varchar(32) NOT NULL COMMENT '店员ID', + `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '提现申请金额(冻结可用收益)', + `fee` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '提现手续费(由租户承担,仅记录)', + `net_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '实付给店员金额(通常等于 amount)', + `dest_account` varchar(128) DEFAULT NULL COMMENT '提现目标(临时:支付宝账号/登录号)', + `status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT '状态:pending/processing/success/failed/canceled', + `out_biz_no` varchar(64) DEFAULT NULL COMMENT '对外幂等号(用于支付宝转账)', + `provider_ref` varchar(128) DEFAULT NULL COMMENT '三方流水号/参考号', + `failure_reason` varchar(512) 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, + KEY `idx_withdrawal_clerk_status` (`clerk_id`, `status`) USING BTREE, + KEY `key_tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='提现请求'; + +-- Freeze policy configuration (tenant default + optional clerk override) +CREATE TABLE IF NOT EXISTS `play_freeze_policy` ( + `id` varchar(32) NOT NULL COMMENT 'UUID', + `tenant_id` varchar(32) NOT NULL COMMENT '租户ID', + `clerk_id` varchar(32) DEFAULT NULL COMMENT '店员ID(为空代表租户默认)', + `freeze_hours` int NOT NULL DEFAULT 24 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, + KEY `idx_policy_tenant_clerk` (`tenant_id`, `clerk_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='收益冻结策略'; + +-- Tenant Alipay config (placeholder; integration not implemented yet) +CREATE TABLE IF NOT EXISTS `play_tenant_alipay_config` ( + `id` varchar(32) NOT NULL COMMENT 'UUID', + `tenant_id` varchar(32) NOT NULL COMMENT '租户ID', + `app_id` varchar(64) DEFAULT NULL COMMENT '支付宝应用APP_ID', + `merchant_private_key` text COMMENT '商户私钥(加密存储,预留)', + `alipay_public_key` text COMMENT '支付宝公钥(预留)', + `notify_url` varchar(255) 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_tenant_alipay` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='租户支付宝配置(暂未实现)'; + diff --git a/play-admin/src/main/resources/db/migration/V4__withdrawal_logs.sql b/play-admin/src/main/resources/db/migration/V4__withdrawal_logs.sql new file mode 100644 index 0000000..4565139 --- /dev/null +++ b/play-admin/src/main/resources/db/migration/V4__withdrawal_logs.sql @@ -0,0 +1,20 @@ +-- Withdrawal logs to track lifecycle and audit +CREATE TABLE IF NOT EXISTS `play_withdrawal_log` ( + `id` varchar(32) NOT NULL COMMENT 'UUID', + `tenant_id` varchar(32) NOT NULL COMMENT '租户ID', + `withdrawal_id` varchar(32) NOT NULL COMMENT '提现请求ID', + `clerk_id` varchar(32) NOT NULL COMMENT '店员ID', + `event_type` varchar(64) NOT NULL COMMENT '事件类型,如 CREATED/RESERVED/PAYOUT_REQUESTED/PAYOUT_SUCCESS/PAYOUT_FAILED', + `status_from` varchar(16) DEFAULT NULL COMMENT '原状态', + `status_to` varchar(16) DEFAULT NULL COMMENT '新状态', + `message` varchar(512) DEFAULT NULL COMMENT '简要信息', + `payload` text COMMENT '详细负载(JSON)', + `created_by` varchar(32) DEFAULT NULL, + `created_time` datetime DEFAULT CURRENT_TIMESTAMP, + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', + `version` int NOT NULL DEFAULT '1' COMMENT '数据版本', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_withdrawal_log_wid` (`withdrawal_id`) USING BTREE, + KEY `key_tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='提现流水日志'; + diff --git a/play-admin/src/main/resources/db/migration/V5__add_earning_type_to_earnings_line.sql b/play-admin/src/main/resources/db/migration/V5__add_earning_type_to_earnings_line.sql new file mode 100644 index 0000000..92f8481 --- /dev/null +++ b/play-admin/src/main/resources/db/migration/V5__add_earning_type_to_earnings_line.sql @@ -0,0 +1,17 @@ +-- Add earning type column to earnings ledger +ALTER TABLE `play_earnings_line` + ADD COLUMN `earning_type` varchar(16) NOT NULL DEFAULT 'ORDER' COMMENT '收益类型(ORDER: 订单收益;COMMISSION: 提成收益)' AFTER `amount`; + +-- Ensure existing数据标记为订单收益 +UPDATE `play_earnings_line` +SET `earning_type` = 'ORDER' +WHERE `earning_type` IS NULL; + +-- 标记工资单相关表已废弃(仅保留历史数据) +ALTER TABLE `play_clerk_wages_info` + COMMENT='店员工资结算信息(Deprecated,仅保留历史数据)'; + +ALTER TABLE `play_clerk_wages_details_info` + COMMENT='店员工资明细信息(Deprecated,仅保留历史数据)'; + +-- 若不存在提成收益,可留待后续插入 diff --git a/play-admin/src/main/resources/db/migration/V6__add_updated_columns_to_withdrawal_log.sql b/play-admin/src/main/resources/db/migration/V6__add_updated_columns_to_withdrawal_log.sql new file mode 100644 index 0000000..baa4a79 --- /dev/null +++ b/play-admin/src/main/resources/db/migration/V6__add_updated_columns_to_withdrawal_log.sql @@ -0,0 +1,4 @@ +-- Align withdrawal log table with BaseEntity audit fields +ALTER TABLE `play_withdrawal_log` + ADD COLUMN `updated_by` varchar(32) DEFAULT NULL AFTER `created_time`, + ADD COLUMN `updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER `updated_by`; diff --git a/play-common/src/main/java/com/starry/common/result/TypedR.java b/play-common/src/main/java/com/starry/common/result/TypedR.java new file mode 100644 index 0000000..f3d90fe --- /dev/null +++ b/play-common/src/main/java/com/starry/common/result/TypedR.java @@ -0,0 +1,95 @@ +package com.starry.common.result; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.beans.ConstructorProperties; +import java.io.Serializable; +import java.util.List; +import lombok.Data; + +/** + * Generic API response wrapper for better Swagger typing. + */ +@Data +@ApiModel(value = "统一返回结果(泛型)") +public class TypedR implements Serializable { + public static final String OK_MSG = "请求成功"; + public static final String FAIL_MSG = "请求失败"; + + @ApiModelProperty(value = "是否成功") + private boolean success; + + @ApiModelProperty(value = "返回码") + private Integer code; + + @ApiModelProperty(value = "返回消息") + private String message; + + @ApiModelProperty(value = "返回数据") + private T data; + + @ApiModelProperty(value = "总条数") + private Long total; + + @ApiModelProperty(value = "分页信息") + private PageInfo pageInfo; + + public TypedR() {} + + private TypedR(int code, boolean success, String message, T data) { + this.code = code; + this.success = success; + this.message = message; + this.data = data; + } + + public static TypedR ok(T data) { + return new TypedR<>(ResultCodeEnum.SUCCESS.getCode(), true, ResultCodeEnum.SUCCESS.getMessage(), data); + } + + public static TypedR okMessage(String msg, T data) { + return new TypedR<>(ResultCodeEnum.SUCCESS.getCode(), true, msg, data); + } + + /** + * Build a list response from MyBatis-Plus page while flattening records/total/pageInfo. + */ + public static TypedR> okPage(IPage page) { + TypedR> r = new TypedR<>(ResultCodeEnum.SUCCESS.getCode(), true, + ResultCodeEnum.SUCCESS.getMessage(), page.getRecords()); + r.setTotal(page.getTotal()); + r.setPageInfo(new PageInfo((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), page.getPages())); + return r; + } + + public static TypedR error(String msg) { + return new TypedR<>(ResultCodeEnum.FAILED.getCode(), false, msg, null); + } + + public static TypedR error(int errorCode, String msg) { + return new TypedR<>(errorCode, false, msg, null); + } + + @Data + public static class PageInfo { + @ApiModelProperty("当前页") + protected int currentPage; + @ApiModelProperty("页大小") + protected int pageSize; + @ApiModelProperty("总记录数") + protected long totalCount; + @ApiModelProperty("总页数") + protected long totalPage; + + public PageInfo() {} + + @ConstructorProperties({"currentPage", "pageSize", "totalCount", "totalPage"}) + public PageInfo(int currentPage, int pageSize, long totalCount, long totalPage) { + this.currentPage = currentPage; + this.pageSize = pageSize; + this.totalCount = totalCount; + this.totalPage = totalPage; + } + } +}