diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java index 6b45bbf..79489ed 100644 --- a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java +++ b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java @@ -40,6 +40,10 @@ public class Attachment extends UUIDEntity { GAO_EVENT_RECORD, + GAO_PACKAGE_DELIVERY, + + GAO_FUND_RECORD, + TEMP_FILE, MIRROR diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java index 66162fb..7ca5690 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java +++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java @@ -21,13 +21,13 @@ public enum GaoPermissionCode implements BuiltinPermissionCode { STORE_DELETE("STORE:DELETE", "门店删除", false, true), - STORE_DIARY_CREATE("STORE_DIARY:CREATE", "门店日记创建", true, true), + STORE_DIARY_CREATE("STORE_DIARY:CREATE", "门店日记创建", true, false), - STORE_DIARY_READ("STORE_DIARY:READ", "门店日记读取", true, true), + STORE_DIARY_READ("STORE_DIARY:READ", "门店日记读取", true, false), - STORE_DIARY_UPDATE("STORE_DIARY:UPDATE", "门店日记修改", true, true), + STORE_DIARY_UPDATE("STORE_DIARY:UPDATE", "门店日记修改", true, false), - STORE_DIARY_DELETE("STORE_DIARY:DELETE", "门店日记删除", true, true), + STORE_DIARY_DELETE("STORE_DIARY:DELETE", "门店日记删除", true, false), CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建", false, false), @@ -59,6 +59,40 @@ public enum GaoPermissionCode implements BuiltinPermissionCode { EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false), + PACKAGE_CONFIG_CREATE("PACKAGE_CONFIG:CREATE", "套餐配置创建", true, true), + + PACKAGE_CONFIG_READ("PACKAGE_CONFIG:READ", "套餐配置读取", true, true), + + PACKAGE_CONFIG_UPDATE("PACKAGE_CONFIG:UPDATE", "套餐配置修改", true, true), + + PACKAGE_CONFIG_DELETE("PACKAGE_CONFIG:DELETE", "套餐配置删除", true, true), + + CUSTOMER_PACKAGE_CREATE("CUSTOMER_PACKAGE:CREATE", "客户套餐创建", true, true), + + CUSTOMER_PACKAGE_READ("CUSTOMER_PACKAGE:READ", "客户套餐读取", true, true), + + CUSTOMER_PACKAGE_CANCEL("CUSTOMER_PACKAGE:CANCEL", "客户套餐取消", true, true), + + CUSTOMER_PACKAGE_PRICE_OVERRIDE("CUSTOMER_PACKAGE:PRICE_OVERRIDE", "客户套餐成交价和产品定制", true, true), + + PACKAGE_DELIVERY_CREATE("PACKAGE_DELIVERY:CREATE", "套餐发放创建", true, true), + + PACKAGE_DELIVERY_READ("PACKAGE_DELIVERY:READ", "套餐发放读取", true, true), + + PACKAGE_DELIVERY_VERIFY("PACKAGE_DELIVERY:VERIFY", "套餐发放核销", true, true), + + PACKAGE_DELIVERY_REVOKE("PACKAGE_DELIVERY:REVOKE", "套餐发放冲正", true, true), + + FUND_RECORD_READ("FUND_RECORD:READ", "资金流水读取", true, true), + + FUND_RECEIVE_CREATE("FUND_RECEIVE:CREATE", "线下收款创建", true, true), + + FUND_RECEIVE_VERIFY("FUND_RECEIVE:VERIFY", "线下收款核销", true, true), + + FUND_REFUND("FUND:REFUND", "套餐退款", true, true), + + FUND_REVOKE("FUND:REVOKE", "资金流水冲正", true, true), + POINT_ACCOUNT_READ("POINT_ACCOUNT:READ", "客户积分账户读取", true, true), POINT_LEDGER_READ("POINT_LEDGER:READ", "客户积分流水读取", true, true), diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java new file mode 100644 index 0000000..4201756 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java @@ -0,0 +1,265 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; +import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; +import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; +import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; +import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest; +import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/// GAO 客户套餐和收退款接口 +/// +/// @author Codex +/// @since 2026-08-31 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/customer/package") +public class GaoCustomerPackageController { + + private final GaoCustomerPackageService service; + private final GaoCustomerService customerService; + private final GaoEventRecordService eventRecordService; + private final GaoPackageDeliveryService deliveryService; + private final GaoFundRecordService fundRecordService; + private final AttachmentService attachmentService; + private final GaoStoreScopeService storeScopeService; + private final UserService userService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoCustomerPackagePage page) { + TimiException.required(page, "not found page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + PageResult result = service.pageByQuery(page); + if (result.getList().isEmpty()) { + return result; + } + fillTransient(result.getList()); + Map customerMap = customerService.mapByIdList(result.getList().stream().map(GaoCustomerPackage::getCustomerId).toList()); + for (GaoCustomerPackage packageEntity : result.getList()) { + packageEntity.setCustomer(customerMap.get(packageEntity.getCustomerId())); + } + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_READ) + @PostMapping("/detail") + public GaoCustomerPackage detail(@RequestParam String id) { + GaoCustomerPackage result = service.get(id); + TimiException.required(result, "not found customer package"); + storeScopeService.checkStoreId(result.getStoreId()); + fillTransient(List.of(result)); + GaoCustomer customer = customerService.get(result.getCustomerId()); + if (customer != null) { + customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); + } + result.setCustomer(customer); + GaoEventRecord purchaseRecord = eventRecordService.get(result.getPurchaseEventRecordId()); + if (purchaseRecord != null) { + purchaseRecord.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, purchaseRecord.getId())); + } + result.setPurchaseEventRecord(purchaseRecord); + result.setDeliveryList(deliveryService.listByCustomerPackageId(result.getId())); + fillDeliveryTransient(result.getDeliveryList()); + result.setFundRecordList(fundRecordService.listByCustomerPackageId(result.getId())); + fillFundTransient(result.getFundRecordList()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_CREATE) + @PostMapping("/purchase") + public GaoCustomerPackage purchase(@RequestBody GaoCustomerPackagePurchaseRequest request) { + GaoCustomerPackage result = service.purchase(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_CANCEL) + @PostMapping("/cancel") + public GaoCustomerPackage cancel(@RequestBody GaoCustomerPackageCancelRequest request) { + GaoCustomerPackage result = service.cancel(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) + @PostMapping("/payment/receive") + public GaoCustomerPackage receive(@RequestBody GaoFundReceiveRequest request) { + fundRecordService.receive(request); + return packageResponse(request.getCustomerPackageId()); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) + @PostMapping("/payment/verify") + public GaoCustomerPackage verifyPayment(@RequestParam String id) { + fundRecordService.verify(id); + return packageAfterFundAction(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) + @PostMapping("/payment/reject") + public GaoCustomerPackage rejectPayment(@RequestBody GaoFundActionRequest request) { + fundRecordService.reject(request); + return packageAfterFundAction(request.getFundRecordId()); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) + @PostMapping("/payment/cancel") + public GaoCustomerPackage cancelPayment(@RequestBody GaoFundActionRequest request) { + fundRecordService.cancel(request); + return packageAfterFundAction(request.getFundRecordId()); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) + @PostMapping("/refund") + public GaoCustomerPackage refund(@RequestBody GaoFundRefundRequest request) { + fundRecordService.refund(request); + return packageResponse(request.getCustomerPackageId()); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) + @PostMapping("/refund/confirm-none") + public GaoCustomerPackage confirmNoRefund(@RequestBody GaoFundConfirmNoRefundRequest request) { + fundRecordService.confirmNoRefund(request); + return packageResponse(request.getCustomerPackageId()); + } + + private GaoCustomerPackage packageAfterFundAction(String fundRecordId) { + var record = fundRecordService.get(fundRecordId); + TimiException.required(record, "not found fund record"); + return packageResponse(record.getBizId()); + } + + private GaoCustomerPackage packageResponse(String packageId) { + GaoCustomerPackage result = service.get(packageId); + TimiException.required(result, "not found customer package"); + fillTransient(List.of(result)); + return result; + } + + private void fillTransient(List packageList) { + if (packageList == null || packageList.isEmpty()) { + return; + } + Map> itemMap = service.mapItemListByCustomerPackageIdList(packageList.stream().map(GaoCustomerPackage::getId).toList()); + List userIdList = packageList.stream() + .flatMap(packageEntity -> Stream.of(packageEntity.getOperatorUserId(), packageEntity.getCancelOperatorUserId())) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoCustomerPackage packageEntity : packageList) { + List itemList = itemMap.getOrDefault(packageEntity.getId(), List.of()); + for (GaoCustomerPackageItem item : itemList) { + fillDerived(item); + } + packageEntity.setItemList(itemList); + packageEntity.setOperator(userMap.get(packageEntity.getOperatorUserId())); + packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId())); + } + } + + private void fillDeliveryTransient(List deliveryList) { + if (deliveryList == null || deliveryList.isEmpty()) { + return; + } + List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList(); + Map> itemMap = deliveryService.mapItemListByDeliveryIdList(idList); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList); + List userIdList = deliveryList.stream() + .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId())) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoPackageDelivery delivery : deliveryList) { + delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of())); + delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of())); + delivery.setOperator(userMap.get(delivery.getOperatorUserId())); + delivery.setVerifier(userMap.get(delivery.getVerifierUserId())); + } + } + + private void fillFundTransient(List recordList) { + if (recordList == null || recordList.isEmpty()) { + return; + } + List idList = recordList.stream().map(GaoFundRecord::getId).toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList); + List userIdList = recordList.stream() + .flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId())) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoFundRecord record : recordList) { + record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); + record.setOperator(userMap.get(record.getOperatorUserId())); + record.setVerifier(userMap.get(record.getVerifierUserId())); + } + } + + private void fillDerived(GaoCustomerPackageItem item) { + BigDecimal reserved = item.getReservedQuantity() == null ? BigDecimal.ZERO : item.getReservedQuantity(); + BigDecimal delivered = item.getDeliveredQuantity() == null ? BigDecimal.ZERO : item.getDeliveredQuantity(); + BigDecimal total = item.getTotalQuantity() == null ? BigDecimal.ZERO : item.getTotalQuantity(); + item.setPendingQuantity(reserved); + item.setAvailableQuantity(total.subtract(reserved).subtract(delivered)); + item.setRemainingQuantity(total.subtract(delivered)); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java new file mode 100644 index 0000000..11686dc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java @@ -0,0 +1,95 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; +import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/// GAO 资金流水接口 +/// +/// @author Codex +/// @since 2026-08-31 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/fund/record") +public class GaoFundRecordController { + + private final GaoFundRecordService service; + private final GaoStoreScopeService storeScopeService; + private final AttachmentService attachmentService; + private final UserService userService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoFundRecordPage page) { + TimiException.required(page, "not found page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + PageResult result = service.pageByQuery(page); + fillTransient(result.getList()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ) + @PostMapping("/detail") + public GaoFundRecord detail(@RequestParam String id) { + GaoFundRecord result = service.get(id); + TimiException.required(result, "not found fund record"); + storeScopeService.checkStoreId(result.getStoreId()); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE) + @PostMapping("/revoke") + public GaoFundRecord revoke(@RequestBody GaoFundRevokeRequest request) { + GaoFundRecord result = service.revoke(request); + fillTransient(List.of(result)); + return result; + } + + private void fillTransient(List recordList) { + if (recordList == null || recordList.isEmpty()) { + return; + } + List idList = recordList.stream().map(GaoFundRecord::getId).toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList); + List userIdList = recordList.stream() + .flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId())) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoFundRecord record : recordList) { + record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); + record.setOperator(userMap.get(record.getOperatorUserId())); + record.setVerifier(userMap.get(record.getVerifierUserId())); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java new file mode 100644 index 0000000..a638a19 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java @@ -0,0 +1,121 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; +import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/// GAO 套餐配置接口 +/// +/// @author Codex +/// @since 2026-08-31 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/package/config") +public class GaoPackageConfigController { + + private final GaoPackageConfigService service; + private final GaoStoreScopeService storeScopeService; + private final UserService userService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoPackageConfigPage page) { + TimiException.required(page, "not found page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + PageResult result = service.pageByQuery(page); + fillTransient(result.getList()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ) + @PostMapping("/list/valid") + public List listValid(@RequestParam String storeId) { + List result = service.listValid(storeScopeService.resolveStoreId(storeId)); + fillTransient(result); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ) + @PostMapping("/detail") + public GaoPackageConfig detail(@RequestParam String id) { + GaoPackageConfig config = service.get(id); + TimiException.required(config, "not found package config"); + storeScopeService.checkStoreId(config.getStoreId()); + fillTransient(List.of(config)); + return config; + } + + private void fillTransient(List configList) { + if (configList == null || configList.isEmpty()) { + return; + } + Map> itemMap = service.mapItemListByPackageConfigIdList(configList.stream().map(GaoPackageConfig::getId).toList()); + List updaterIdList = configList.stream().map(GaoPackageConfig::getUpdatedBy).filter(value -> value != null && !value.isBlank()).distinct().toList(); + Map updaterMap = updaterIdList.isEmpty() ? Map.of() : userService.mapByIdList(updaterIdList); + for (GaoPackageConfig config : configList) { + config.setItemList(itemMap.getOrDefault(config.getId(), List.of())); + config.setUpdater(updaterMap.get(config.getUpdatedBy())); + } + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_CREATE) + @PostMapping("/create") + public String create(@RequestBody GaoPackageConfig config) { + TimiException.required(config, "not found package config"); + config.setStoreId(storeScopeService.resolveStoreId(config.getStoreId())); + service.create(config); + return config.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoPackageConfig config) { + TimiException.required(config, "not found package config"); + TimiException.required(config.getId(), "not found packageConfig.id"); + GaoPackageConfig dbConfig = service.get(config.getId()); + TimiException.required(dbConfig, "not found package config"); + storeScopeService.checkStoreId(dbConfig.getStoreId()); + config.setStoreId(dbConfig.getStoreId()); + service.update(config); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_DELETE) + @PostMapping("/delete") + public void delete(@RequestParam String id) { + GaoPackageConfig config = service.get(id); + TimiException.required(config, "not found package config"); + storeScopeService.checkStoreId(config.getStoreId()); + service.delete(id); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java new file mode 100644 index 0000000..716e04b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java @@ -0,0 +1,140 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; +import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; +import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/// GAO 套餐发放接口 +/// +/// @author Codex +/// @since 2026-08-31 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/package/delivery") +public class GaoPackageDeliveryController { + + private final GaoPackageDeliveryService service; + private final GaoStoreScopeService storeScopeService; + private final AttachmentService attachmentService; + private final UserService userService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoPackageDeliveryPage page) { + TimiException.required(page, "not found page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + PageResult result = service.pageByQuery(page); + fillTransient(result.getList()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ) + @PostMapping("/detail") + public GaoPackageDelivery detail(@RequestParam String id) { + GaoPackageDelivery result = service.get(id); + TimiException.required(result, "not found delivery"); + storeScopeService.checkStoreId(result.getStoreId()); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE) + @PostMapping("/create") + public GaoPackageDelivery create(@RequestBody GaoPackageDeliveryCreateRequest request) { + GaoPackageDelivery result = service.createDelivery(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY) + @PostMapping("/verify") + public GaoPackageDelivery verify(@RequestParam String id) { + GaoPackageDelivery result = service.verify(id); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY) + @PostMapping("/reject") + public GaoPackageDelivery reject(@RequestBody GaoPackageDeliveryActionRequest request) { + GaoPackageDelivery result = service.reject(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE) + @PostMapping("/cancel") + public GaoPackageDelivery cancel(@RequestBody GaoPackageDeliveryActionRequest request) { + GaoPackageDelivery result = service.cancel(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_REVOKE) + @PostMapping("/revoke") + public GaoPackageDelivery revoke(@RequestBody GaoPackageDeliveryRevokeRequest request) { + GaoPackageDelivery result = service.revoke(request); + fillTransient(List.of(result)); + return result; + } + + private void fillTransient(List deliveryList) { + if (deliveryList == null || deliveryList.isEmpty()) { + return; + } + List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList(); + Map> itemMap = service.mapItemListByDeliveryIdList(idList); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList); + List userIdList = deliveryList.stream() + .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId())) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoPackageDelivery delivery : deliveryList) { + delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of())); + delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of())); + delivery.setOperator(userMap.get(delivery.getOperatorUserId())); + delivery.setVerifier(userMap.get(delivery.getVerifierUserId())); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java new file mode 100644 index 0000000..facd096 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java @@ -0,0 +1,145 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 客户实际购买的套餐 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoCustomerPackage extends UUIDEntity { + + /// 客户套餐状态 + public enum Status { + + /// 履约中 + ACTIVE, + + /// 已全部履约 + FULFILLED, + + /// 已取消 + CANCELLED + } + + /// 付款状态 + public enum PaymentStatus { + + /// 未付款 + UNPAID, + + /// 部分付款 + PARTIALLY_PAID, + + /// 已付清 + PAID + } + + /// 退款状态 + public enum RefundStatus { + + /// 无需退款 + NOT_REQUIRED, + + /// 待退款或待确认无需退款 + PENDING, + + /// 已完成退款处理 + REFUNDED + } + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 来源套餐配置 ID + private String packageConfigId; + + /// 购买事件登记 ID + private String purchaseEventRecordId; + + /// 购买幂等请求号 + private String requestNo; + + /// 套餐名称快照 + private String packageNameSnapshot; + + /// 标准价快照,单位分 + private Long standardAmountSnapshot; + + /// 实际成交价,单位分 + private Long saleAmount; + + /// 待核销收款合计,单位分 + private Long pendingPaidAmount; + + /// 累计有效收款,单位分 + private Long paidAmount; + + /// 累计有效退款,单位分 + private Long refundedAmount; + + /// 套餐状态 + private Status status; + + /// 付款状态 + private PaymentStatus paymentStatus; + + /// 退款状态 + private RefundStatus refundStatus; + + /// 购买时间 + private Long purchasedAt; + + /// 购买登记人 + private String operatorUserId; + + /// 取消时间 + private Long cancelledAt; + + /// 取消操作人 + private String cancelOperatorUserId; + + /// 取消原因 + private String cancelReason; + + /// 备注 + private String remark; + + /// 客户 + @Transient + private GaoCustomer customer; + + /// 购买事件登记 + @Transient + private GaoEventRecord purchaseEventRecord; + + /// 购买操作人 + @Transient + private User operator; + + /// 取消操作人 + @Transient + private User cancelOperator; + + /// 产品权益列表 + @Transient + private List itemList; + + /// 发放历史 + @Transient + private List deliveryList; + + /// 资金历史 + @Transient + private List fundRecordList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java new file mode 100644 index 0000000..450f06a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java @@ -0,0 +1,53 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 客户套餐产品权益快照 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoCustomerPackageItem extends UUIDEntity { + + /// 客户套餐 ID + private String customerPackageId; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 产品单位快照 + private String unitSnapshot; + + /// 总权益数量 + private BigDecimal totalQuantity; + + /// 待核销占用数量 + private BigDecimal reservedQuantity; + + /// 已核销净发放数量 + private BigDecimal deliveredQuantity; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; + + /// 待核销数量 + @Transient + private BigDecimal pendingQuantity; + + /// 当前可发数量 + @Transient + private BigDecimal availableQuantity; + + /// 权益剩余数量 + @Transient + private BigDecimal remainingQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java index 1eb6102..00694c6 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java @@ -18,6 +18,16 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public class GaoEvent extends UUIDEntity { + /// 事件业务类型 + public enum BizType { + + /// 普通登记事件 + GENERAL, + + /// 购买产品套餐 + PACKAGE_PURCHASE + } + /// /// 登记事件状态 /// @@ -93,6 +103,9 @@ public class GaoEvent extends UUIDEntity { /// 统计图表颜色 private String color; + /// 业务类型 + private BizType bizType; + /// 登记事件名称 private String name; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java new file mode 100644 index 0000000..7292ca0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java @@ -0,0 +1,115 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 通用资金操作流水 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoFundRecord extends UUIDEntity { + + /// 资金业务类型 + public enum BizType { + + /// 客户套餐 + CUSTOMER_PACKAGE + } + + /// 资金操作类型 + public enum OperationType { + + /// 线下收款 + RECEIVE, + + /// 退款 + REFUND, + + /// 确认无需退款 + CONFIRM_NO_REFUND, + + /// 资金操作冲正 + REVOKE + } + + /// 资金操作状态 + public enum Status { + + /// 待店长核销 + PENDING, + + /// 已生效 + VERIFIED, + + /// 店长已拒绝 + REJECTED, + + /// 提交人已撤销 + CANCELLED + } + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 资金业务类型 + private BizType bizType; + + /// 业务对象 ID + private String bizId; + + /// 操作类型 + private OperationType operationType; + + /// 操作状态 + private Status status; + + /// 金额,单位分 + private Long amount; + + /// 冲正时关联原资金记录 + private String sourceRecordId; + + /// 幂等请求号 + private String requestNo; + + /// 提交操作的用户 ID + private String operatorUserId; + + /// 收款核销用户 ID + private String verifierUserId; + + /// 收款核销时间 + private Long verifiedAt; + + /// 线下操作发生时间 + private Long occurredAt; + + /// 原因 + private String reason; + + /// 备注 + private String remark; + + /// 操作人 + @Transient + private User operator; + + /// 核销人 + @Transient + private User verifier; + + /// 附件 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfig.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfig.java new file mode 100644 index 0000000..406fe5c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfig.java @@ -0,0 +1,66 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 套餐配置 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPackageConfig extends UUIDEntity { + + /// 套餐配置状态 + public enum Status { + + /// 草稿,不可销售 + DRAFT, + + /// 启用,可在有效期内销售 + ACTIVE, + + /// 停用,不可销售 + INACTIVE + } + + /// 门店 ID + private String storeId; + + /// 套餐名称 + private String name; + + /// 套餐说明 + private String description; + + /// 标准总价,单位分 + private Long standardAmount; + + /// 状态 + private Status status; + + /// 生效开始时间戳,毫秒,空为不限制 + private Long beginAt; + + /// 生效结束时间戳,毫秒,空为不限制 + private Long endAt; + + /// 排序值 + private Integer sort; + + /// 最后修改用户 ID + private String updatedBy; + + /// 最后修改用户 + @Transient + private User updater; + + /// 配置产品列表 + @Transient + private List itemList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java new file mode 100644 index 0000000..205d250 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java @@ -0,0 +1,34 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 套餐配置产品明细 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPackageConfigItem extends UUIDEntity { + + /// 套餐配置 ID + private String packageConfigId; + + /// 产品名称 + private String productName; + + /// 产品单位 + private String unit; + + /// 默认数量 + private BigDecimal defaultQuantity; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java new file mode 100644 index 0000000..34f2b6e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java @@ -0,0 +1,107 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 套餐发放操作 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPackageDelivery extends UUIDEntity { + + /// 发放操作类型 + public enum OperationType { + + /// 正常发放 + DELIVER, + + /// 整单冲正 + REVOKE + } + + /// 发放状态 + public enum Status { + + /// 待核销 + PENDING, + + /// 已核销 + VERIFIED, + + /// 店长已拒绝 + REJECTED, + + /// 提交人已撤销 + CANCELLED, + + /// 已核销后被整单冲正 + REVOKED + } + + /// 客户套餐 ID + private String customerPackageId; + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 操作类型 + private OperationType operationType; + + /// 发放状态 + private Status status; + + /// 客户端幂等请求号 + private String requestNo; + + /// 提交人 + private String operatorUserId; + + /// 实际发放时间 + private Long deliveredAt; + + /// 核销或拒绝操作人 + private String verifierUserId; + + /// 核销或拒绝时间 + private Long verifiedAt; + + /// 冲正单指向原发放单 + private String sourceDeliveryId; + + /// 拒绝、撤销或冲正原因 + private String reason; + + /// 备注 + private String remark; + + /// 操作人 + @Transient + private User operator; + + /// 核销或拒绝操作人 + @Transient + private User verifier; + + /// 原发放单 + @Transient + private GaoPackageDelivery sourceDelivery; + + /// 发放明细 + @Transient + private List itemList; + + /// 附件 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java new file mode 100644 index 0000000..f1fab81 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java @@ -0,0 +1,43 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 套餐发放产品明细 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPackageDeliveryItem extends UUIDEntity { + + /// 发放单 ID + private String deliveryId; + + /// 客户套餐产品 ID + private String customerPackageItemId; + + /// 本次数量 + private BigDecimal quantity; + + /// 操作前占用数量快照 + private BigDecimal reservedBefore; + + /// 操作后占用数量快照 + private BigDecimal reservedAfter; + + /// 操作前已发数量快照 + private BigDecimal deliveredBefore; + + /// 操作后已发数量快照 + private BigDecimal deliveredAfter; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 产品单位快照 + private String unitSnapshot; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java new file mode 100644 index 0000000..79f9ba4 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java @@ -0,0 +1,30 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 客户套餐产品权益 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoCustomerPackageItemMapper extends BaseMapper { + + /// 按客户套餐查询产品权益 + List selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId); + + /// 批量查询客户套餐产品权益 + List selectByCustomerPackageIdList(@Param("customerPackageIdList") Collection customerPackageIdList); + + /// 锁定客户套餐产品 + GaoCustomerPackageItem selectForUpdate(@Param("id") String id); + + /// 调整待核销占用数量 + int updateReservedQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta); + + /// 调整已核销发放数量 + int updateDeliveredQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java new file mode 100644 index 0000000..39ac742 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +/// GAO 客户套餐 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoCustomerPackageMapper extends BaseMapper { + + /// 按门店和请求号查询购买结果 + GaoCustomerPackage selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 按请求号查询购买结果,用于配置删除后的幂等重试 + GaoCustomerPackage selectByRequestNoAnyStore(@Param("requestNo") String requestNo); + + /// 锁定客户套餐 + GaoCustomerPackage selectForUpdate(@Param("id") String id); + + /// 按购买事件登记查询客户套餐 + GaoCustomerPackage selectByPurchaseEventRecordId(@Param("purchaseEventRecordId") String purchaseEventRecordId); + + /// 按条件统计客户套餐 + long countByQuery(GaoCustomerPackagePage page); + + /// 按条件查询客户套餐 + java.util.List selectByQuery(GaoCustomerPackagePage page); + + /// 原子调整待核销收款 + int updatePendingPaidAmountDelta(@Param("id") String id, @Param("delta") Long delta); + + /// 原子调整有效收款 + int updatePaidAmountDelta(@Param("id") String id, @Param("delta") Long delta); + + /// 带状态条件更新套餐状态 + int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoCustomerPackage.Status fromStatus, @Param("toStatus") GaoCustomerPackage.Status toStatus, @Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId, @Param("cancelReason") String cancelReason, @Param("refundStatus") GaoCustomerPackage.RefundStatus refundStatus); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java new file mode 100644 index 0000000..4f130b0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java @@ -0,0 +1,39 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 资金流水 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoFundRecordMapper extends BaseMapper { + + /// 按门店和请求号查询资金操作 + GaoFundRecord selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 锁定资金记录 + GaoFundRecord selectForUpdate(@Param("id") String id); + + /// 查询资金冲正记录 + GaoFundRecord selectRevokeBySourceRecordId(@Param("sourceRecordId") String sourceRecordId); + + /// 查询客户套餐待核销收款数量 + long countPendingReceiveByBizId(@Param("bizId") String bizId); + + /// 查询客户套餐资金历史 + List selectByBizId(@Param("bizId") String bizId); + + /// 按条件统计资金记录 + long countByQuery(GaoFundRecordPage page); + + /// 按条件查询资金记录 + List selectByQuery(GaoFundRecordPage page); + + /// 带状态条件更新资金记录 + int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoFundRecord.Status fromStatus, @Param("toStatus") GaoFundRecord.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigItemMapper.java new file mode 100644 index 0000000..a525b8b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigItemMapper.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 套餐配置产品 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoPackageConfigItemMapper extends BaseMapper { + + /// 按套餐配置 ID 查询产品 + List selectByPackageConfigId(@Param("packageConfigId") String packageConfigId); + + /// 批量查询套餐配置产品 + List selectByPackageConfigIdList(@Param("packageConfigIdList") Collection packageConfigIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigMapper.java new file mode 100644 index 0000000..5d8ac9b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigMapper.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; +import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 套餐配置 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoPackageConfigMapper extends BaseMapper { + + /// 按 ID 列表查询套餐配置 + List selectByIdList(@Param("idList") Collection idList); + + /// 查询有效可购买套餐 + List selectValidList(@Param("storeId") String storeId, @Param("now") Long now); + + /// 按条件统计套餐配置 + long countByQuery(GaoPackageConfigPage page); + + /// 按条件查询套餐配置 + List selectByQuery(GaoPackageConfigPage page); + + /// 查询门店内同名配置 + GaoPackageConfig selectByStoreIdAndName(@Param("storeId") String storeId, @Param("name") String name, @Param("excludeId") String excludeId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java new file mode 100644 index 0000000..ebee2fb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 套餐发放明细 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoPackageDeliveryItemMapper extends BaseMapper { + + /// 按发放单查询明细 + List selectByDeliveryId(@Param("deliveryId") String deliveryId); + + /// 批量查询发放明细 + List selectByDeliveryIdList(@Param("deliveryIdList") Collection deliveryIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java new file mode 100644 index 0000000..7ee4923 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java @@ -0,0 +1,42 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 套餐发放 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoPackageDeliveryMapper extends BaseMapper { + + /// 按门店和请求号查询发放结果 + GaoPackageDelivery selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 锁定发放单 + GaoPackageDelivery selectForUpdate(@Param("id") String id); + + /// 查询原发放单的冲正记录 + GaoPackageDelivery selectRevokeBySourceDeliveryId(@Param("sourceDeliveryId") String sourceDeliveryId); + + /// 查询客户套餐待核销发放数量 + long countPendingByCustomerPackageId(@Param("customerPackageId") String customerPackageId); + + /// 查询客户套餐已核销正常发放数量 + long countVerifiedByCustomerPackageId(@Param("customerPackageId") String customerPackageId); + + /// 查询客户套餐发放历史 + java.util.List selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId); + + /// 按条件统计发放单 + long countByQuery(GaoPackageDeliveryPage page); + + /// 按条件查询发放单 + List selectByQuery(GaoPackageDeliveryPage page); + + /// 带状态条件更新发放单 + int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoPackageDelivery.Status fromStatus, @Param("toStatus") GaoPackageDelivery.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreDiaryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreDiaryMapper.java new file mode 100644 index 0000000..39912c9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreDiaryMapper.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.common.entity.Article; +import com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage; +import com.imyeyu.spring.mapper.BaseMapper; + +import java.util.List; + +/// GAO 门店日记 Mapper +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoStoreDiaryMapper extends BaseMapper { + + /// 查询月份内的门店日记 + /// + /// @param page 月份分页查询请求 + /// @return 门店日记列表 + List
selectByMonth(GaoStoreDiaryPage page); + + /// 统计月份内的门店日记数量 + /// + /// @param page 月份分页查询请求 + /// @return 门店日记数量 + long countByMonth(GaoStoreDiaryPage page); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java new file mode 100644 index 0000000..55ba3b2 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java @@ -0,0 +1,87 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/// GAO 客户套餐服务 +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoCustomerPackageService extends BaseService { + + /// 按条件查询客户套餐 + PageResult pageByQuery(GaoCustomerPackagePage page); + + /// 创建客户套餐及购买事件登记 + GaoCustomerPackage purchase(GaoCustomerPackagePurchaseRequest request); + + /// 取消客户套餐 + GaoCustomerPackage cancel(GaoCustomerPackageCancelRequest request); + + /// 锁定客户套餐 + /// + /// @param id 客户套餐 ID + /// @return 已锁定的客户套餐,不存在时为空 + GaoCustomerPackage getForUpdate(String id); + + /// 查询客户套餐产品权益 + /// + /// @param customerPackageId 客户套餐 ID + /// @return 产品权益列表 + List listItemByCustomerPackageId(String customerPackageId); + + /// 批量查询客户套餐产品权益 + /// + /// @param customerPackageIdList 客户套餐 ID 列表 + /// @return 按客户套餐 ID 分组的产品权益 + Map> mapItemListByCustomerPackageIdList(Collection customerPackageIdList); + + /// 锁定客户套餐产品权益 + /// + /// @param itemId 客户套餐产品权益 ID + /// @return 已锁定的产品权益,不存在时为空 + GaoCustomerPackageItem getItemForUpdate(String itemId); + + /// 原子调整待核销发放占用数量 + /// + /// @param itemId 客户套餐产品权益 ID + /// @param delta 调整量 + /// @return 受影响行数 + int updateItemReservedQuantity(String itemId, BigDecimal delta); + + /// 原子调整已核销发放数量 + /// + /// @param itemId 客户套餐产品权益 ID + /// @param delta 调整量 + /// @return 受影响行数 + int updateItemDeliveredQuantity(String itemId, BigDecimal delta); + + /// 原子调整待核销收款金额 + /// + /// @param id 客户套餐 ID + /// @param delta 调整量 + /// @return 受影响行数 + int updatePendingPaidAmountDelta(String id, Long delta); + + /// 原子调整有效收款金额 + /// + /// @param id 客户套餐 ID + /// @param delta 调整量 + /// @return 受影响行数 + int updatePaidAmountDelta(String id, Long delta); + + /// 保存客户套餐聚合状态 + /// + /// @param packageEntity 客户套餐聚合 + void updateAggregateState(GaoCustomerPackage packageEntity); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java index 7db0e8a..eeeed06 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java @@ -41,6 +41,18 @@ public interface GaoEventRecordService extends BaseService { + + /// 按条件查询资金流水 + PageResult pageByQuery(GaoFundRecordPage page); + + /// 查询客户套餐资金历史 + List listByCustomerPackageId(String customerPackageId); + + /// 登记线下收款 + GaoFundRecord receive(GaoFundReceiveRequest request); + + /// 核销收款 + GaoFundRecord verify(String fundRecordId); + + /// 拒绝待核销收款 + GaoFundRecord reject(GaoFundActionRequest request); + + /// 撤销自己提交的待核销收款 + GaoFundRecord cancel(GaoFundActionRequest request); + + /// 登记退款 + GaoFundRecord refund(GaoFundRefundRequest request); + + /// 确认无需退款 + GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request); + + /// 冲正资金流水 + GaoFundRecord revoke(GaoFundRevokeRequest request); + + /// 统计客户套餐待核销收款数量 + /// + /// @param bizId 业务 ID + /// @return 待核销收款数量 + long countPendingReceiveByBizId(String bizId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageConfigService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageConfigService.java new file mode 100644 index 0000000..11c20b8 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageConfigService.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; +import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/// GAO 套餐配置服务 +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoPackageConfigService extends BaseService { + + /// 按条件查询套餐配置 + PageResult pageByQuery(GaoPackageConfigPage page); + + /// 查询有效可购买套餐 + List listValid(String storeId); + + /// 查询套餐配置产品明细 + /// + /// @param packageConfigId 套餐配置 ID + /// @return 产品明细列表 + List listItemByPackageConfigId(String packageConfigId); + + /// 批量查询套餐配置产品明细 + /// + /// @param packageConfigIdList 套餐配置 ID 列表 + /// @return 按套餐配置 ID 分组的产品明细 + Map> mapItemListByPackageConfigIdList(Collection packageConfigIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java new file mode 100644 index 0000000..078e2c5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java @@ -0,0 +1,60 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; +import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/// GAO 套餐发放服务 +/// +/// @author Codex +/// @since 2026-08-31 +public interface GaoPackageDeliveryService extends BaseService { + + /// 按条件查询发放单 + PageResult pageByQuery(GaoPackageDeliveryPage page); + + /// 查询客户套餐发放历史 + List listByCustomerPackageId(String customerPackageId); + + /// 批量查询发放明细 + /// + /// @param deliveryIdList 发放单 ID 列表 + /// @return 按发放单 ID 分组的发放明细 + Map> mapItemListByDeliveryIdList(Collection deliveryIdList); + + /// 创建待核销发放单 + GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request); + + /// 核销发放单 + GaoPackageDelivery verify(String deliveryId); + + /// 拒绝待核销发放单 + GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request); + + /// 撤销自己提交的待核销发放单 + GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request); + + /// 整单冲正已核销发放单 + GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request); + + /// 统计客户套餐待核销发放单数量 + /// + /// @param customerPackageId 客户套餐 ID + /// @return 待核销发放单数量 + long countPendingByCustomerPackageId(String customerPackageId); + + /// 统计客户套餐已核销正常发放单数量 + /// + /// @param customerPackageId 客户套餐 ID + /// @return 已核销正常发放单数量 + long countVerifiedByCustomerPackageId(String customerPackageId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java new file mode 100644 index 0000000..bbd90a0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java @@ -0,0 +1,388 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; +import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageItemMapper; +import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; +import com.imyeyu.api.modules.gao.service.GaoEventService; +import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; +import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; +import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.PermissionChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// GAO 客户套餐服务实现 +/// +/// @author Codex +/// @since 2026-08-31 +@Service +@RequiredArgsConstructor +public class GaoCustomerPackageServiceImplement extends AbstractEntityService implements GaoCustomerPackageService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoCustomerPackageMapper mapper; + private final GaoCustomerPackageItemMapper itemMapper; + private final ObjectProvider deliveryServiceProvider; + private final ObjectProvider fundRecordServiceProvider; + private final GaoPackageConfigService packageConfigService; + private final GaoCustomerService customerService; + private final GaoEventService eventService; + private final GaoEventRecordService eventRecordService; + private final GaoStoreScopeService storeScopeService; + private final UserLoginService userLoginService; + private final PermissionChecker permissionChecker; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public GaoCustomerPackage getForUpdate(String id) { + return mapper.selectForUpdate(id); + } + + @Override + public List listItemByCustomerPackageId(String customerPackageId) { + return itemMapper.selectByCustomerPackageId(customerPackageId); + } + + @Override + public GaoCustomerPackageItem getItemForUpdate(String itemId) { + return itemMapper.selectForUpdate(itemId); + } + + @Override + public int updateItemReservedQuantity(String itemId, BigDecimal delta) { + return itemMapper.updateReservedQuantity(itemId, delta); + } + + @Override + public int updateItemDeliveredQuantity(String itemId, BigDecimal delta) { + return itemMapper.updateDeliveredQuantity(itemId, delta); + } + + @Override + public int updatePendingPaidAmountDelta(String id, Long delta) { + return mapper.updatePendingPaidAmountDelta(id, delta); + } + + @Override + public int updatePaidAmountDelta(String id, Long delta) { + return mapper.updatePaidAmountDelta(id, delta); + } + + @Override + public void updateAggregateState(GaoCustomerPackage packageEntity) { + mapper.update(packageEntity); + } + + /// 客户套餐必须通过购买流程创建 + /// + /// @param packageEntity 客户套餐 + @Override + public void create(GaoCustomerPackage packageEntity) { + TimiException.requiredTrue(false, "客户套餐必须通过购买接口创建"); + } + + /// 客户套餐创建后不允许通用修改 + /// + /// @param packageEntity 客户套餐 + @Override + public void update(GaoCustomerPackage packageEntity) { + TimiException.requiredTrue(false, "客户套餐不允许通用修改"); + } + + /// 客户套餐不允许通用删除 + /// + /// @param id 客户套餐 ID + @Override + public void delete(String id) { + TimiException.requiredTrue(false, "客户套餐不允许删除"); + } + + /// 客户套餐不允许物理删除 + /// + /// @param id 客户套餐 ID + @Override + public void destroy(String id) { + TimiException.requiredTrue(false, "客户套餐不允许物理删除"); + } + + @Override + public PageResult pageByQuery(GaoCustomerPackagePage page) { + TimiException.required(page, "not found page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + return result; + } + + @Override + public GaoCustomerPackage get(String id) { + return super.get(id); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoCustomerPackage purchase(GaoCustomerPackagePurchaseRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_PURCHASE", request, () -> purchaseInternal(request)); + } + + private GaoCustomerPackage purchaseInternal(GaoCustomerPackagePurchaseRequest request) { + TimiException.required(request, "not found purchase request"); + TimiException.required(request.getRequestNo(), "not found requestNo"); + request.setRequestNo(request.getRequestNo().trim()); + TimiException.required(request.getPackageConfigId(), "not found packageConfigId"); + TimiException.required(request.getEventId(), "not found eventId"); + TimiException.required(request.getCustomerId(), "not found customerId"); + TimiException.required(request.getSaleAmount(), "not found saleAmount"); + TimiException.requiredTrue(0 <= request.getSaleAmount(), "成交价不能为负数"); + + GaoPackageConfig config = packageConfigService.get(request.getPackageConfigId()); + if (config == null) { + GaoCustomerPackage existing = mapper.selectByRequestNoAnyStore(request.getRequestNo()); + if (existing != null) { + storeScopeService.checkStoreId(existing.getStoreId()); + verifyPurchaseIdempotency(existing, request); + return existing; + } + } + TimiException.required(config, "not found package config"); + storeScopeService.checkStoreId(config.getStoreId()); + GaoCustomerPackage existing = mapper.selectByRequestNo(config.getStoreId(), request.getRequestNo()); + if (existing != null) { + verifyPurchaseIdempotency(existing, request); + return existing; + } + + Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now()); + Long now = Time.now(); + TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买"); + TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效"); + TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期"); + List configItemList = packageConfigService.listItemByPackageConfigId(config.getId()); + GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId()); + GaoEvent event = validatePurchaseEvent(request.getEventId(), config.getStoreId()); + + List itemList = new java.util.ArrayList<>(TimiJava.defaultIfNull(request.getItemList(), List.of())); + verifyPurchaseItems(itemList); + itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort)); + boolean canOverride = permissionChecker.hasAny(ModuleCode.GAO, com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue()); + if (!canOverride) { + TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价"); + verifySameAsConfig(itemList, configItemList); + } + + GaoEventRecord eventRecord = new GaoEventRecord(); + eventRecord.setCustomerId(customer.getId()); + eventRecord.setStoreId(config.getStoreId()); + eventRecord.setEventId(event.getId()); + eventRecord.setRegisteredAt(purchasedAt); + eventRecord.setRemark(request.getRemark()); + if (event.getValueType() == GaoEvent.ValueType.NUMBER && event.getValueUnit() == GaoEvent.ValueUnit.CENT) { + eventRecord.setNumberValue(request.getSaleAmount()); + } + GaoEventRecord createdEventRecord = eventRecordService.createPackagePurchaseRecord(eventRecordWithAttachments(eventRecord, request)); + + GaoCustomerPackage result = new GaoCustomerPackage(); + result.setStoreId(config.getStoreId()); + result.setCustomerId(customer.getId()); + result.setPackageConfigId(config.getId()); + result.setPurchaseEventRecordId(createdEventRecord.getId()); + result.setRequestNo(request.getRequestNo()); + result.setPackageNameSnapshot(config.getName()); + result.setStandardAmountSnapshot(config.getStandardAmount()); + result.setSaleAmount(request.getSaleAmount()); + result.setPendingPaidAmount(0L); + result.setPaidAmount(0L); + result.setRefundedAmount(0L); + result.setStatus(GaoCustomerPackage.Status.ACTIVE); + result.setPaymentStatus(request.getSaleAmount() == 0 ? GaoCustomerPackage.PaymentStatus.PAID : GaoCustomerPackage.PaymentStatus.UNPAID); + result.setRefundStatus(GaoCustomerPackage.RefundStatus.NOT_REQUIRED); + result.setPurchasedAt(purchasedAt); + result.setOperatorUserId(userLoginService.getRequireLoginUserId()); + result.setRemark(request.getRemark()); + super.create(result); + for (GaoCustomerPackagePurchaseItem source : itemList) { + GaoCustomerPackageItem item = new GaoCustomerPackageItem(); + item.setCustomerPackageId(result.getId()); + item.setProductNameSnapshot(source.getProductName().trim()); + item.setUnitSnapshot(source.getUnit().trim()); + item.setTotalQuantity(source.getTotalQuantity()); + item.setReservedQuantity(ZERO); + item.setDeliveredQuantity(ZERO); + item.setSort(source.getSort()); + item.setRemark(source.getRemark()); + itemMapper.insert(item); + } + return result; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoCustomerPackage cancel(GaoCustomerPackageCancelRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_CANCEL", request, () -> cancelInternal(request)); + } + + private GaoCustomerPackage cancelInternal(GaoCustomerPackageCancelRequest request) { + TimiException.required(request, "not found cancel request"); + TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); + String reason = requiredReason(request.getReason(), "取消原因不能为空"); + GaoCustomerPackage packageEntity = mapper.selectForUpdate(request.getCustomerPackageId()); + TimiException.required(packageEntity, "not found customer package"); + storeScopeService.checkStoreId(packageEntity.getStoreId()); + TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可取消"); + TimiException.requiredTrue(0 == deliveryServiceProvider.getObject().countPendingByCustomerPackageId(packageEntity.getId()), "存在待核销发放,不能取消套餐"); + TimiException.requiredTrue(0 == fundRecordServiceProvider.getObject().countPendingReceiveByBizId(packageEntity.getId()), "存在待核销收款,不能取消套餐"); + TimiException.requiredTrue(0 == pendingAmount(packageEntity), "存在待核销收款,不能取消套餐"); + List itemList = itemMapper.selectByCustomerPackageId(packageEntity.getId()); + for (GaoCustomerPackageItem item : itemList) { + TimiException.requiredTrue(item.getDeliveredQuantity().compareTo(item.getTotalQuantity()) < 0, "已全部履约的套餐不能取消"); + } + packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED); + packageEntity.setCancelledAt(Time.now()); + packageEntity.setCancelOperatorUserId(userLoginService.getRequireLoginUserId()); + packageEntity.setCancelReason(reason); + packageEntity.setRefundStatus(0 < safeLong(packageEntity.getPaidAmount()) ? GaoCustomerPackage.RefundStatus.PENDING : GaoCustomerPackage.RefundStatus.NOT_REQUIRED); + mapper.update(packageEntity); + return packageEntity; + } + + private GaoEventRecord eventRecordWithAttachments(GaoEventRecord record, GaoCustomerPackagePurchaseRequest request) { + record.setAttachmentList(request.getAttachmentList()); + return record; + } + + private GaoCustomer validateCustomer(String customerId, String storeId) { + GaoCustomer customer = customerService.get(customerId); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户和套餐不属于同一门店"); + return customer; + } + + private GaoEvent validatePurchaseEvent(String eventId, String storeId) { + GaoEvent event = eventService.get(eventId); + TimiException.required(event, "not found purchase event"); + TimiException.requiredTrue(storeId.equals(event.getStoreId()), "购买事件和套餐不属于同一门店"); + TimiException.requiredTrue(event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE, "事件不是套餐购买事件"); + return event; + } + + private void verifyPurchaseItems(List itemList) { + TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品"); + Set keySet = new HashSet<>(); + for (int i = 0; i < itemList.size(); i++) { + GaoCustomerPackagePurchaseItem item = itemList.get(i); + TimiException.required(item, "购买产品明细不能为空"); + TimiException.required(item.getProductName(), "购买产品名称不能为空"); + TimiException.required(item.getUnit(), "购买产品单位不能为空"); + TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空"); + TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0"); + TimiException.requiredTrue(3 >= item.getTotalQuantity().scale(), "购买产品数量最多保留三位小数"); + String key = item.getProductName().trim() + "|" + item.getUnit().trim(); + TimiException.requiredTrue(keySet.add(key), "购买套餐内产品名称和单位不能重复"); + item.setProductName(item.getProductName().trim()); + item.setUnit(item.getUnit().trim()); + item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); + } + } + + private void verifySameAsConfig(List itemList, List configItemList) { + TimiException.requiredTrue(itemList.size() == configItemList.size(), "普通员工不能修改套餐产品明细"); + Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity())); + for (GaoPackageConfigItem configItem : configItemList) { + GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit()); + TimiException.required(item, "普通员工不能修改套餐产品明细"); + TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0 + && item.getSort().equals(configItem.getSort()), "普通员工不能修改套餐产品明细"); + } + } + + private void verifyPurchaseIdempotency(GaoCustomerPackage existing, GaoCustomerPackagePurchaseRequest request) { + TimiException.requiredTrue(existing.getPackageConfigId().equals(request.getPackageConfigId()) + && existing.getCustomerId().equals(request.getCustomerId()) + && existing.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用"); + GaoEventRecord eventRecord = eventRecordService.get(existing.getPurchaseEventRecordId()); + TimiException.required(eventRecord, "购买事件登记不存在"); + TimiException.requiredTrue(eventRecord.getEventId().equals(request.getEventId()) + && (request.getPurchasedAt() == null || eventRecord.getRegisteredAt().equals(request.getPurchasedAt())) + && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致"); + List existingItemList = itemMapper.selectByCustomerPackageId(existing.getId()); + List requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of()); + verifyPurchaseItems(requestItemList); + TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致"); + Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductNameSnapshot() + "|" + item.getUnitSnapshot(), Function.identity())); + for (GaoCustomerPackagePurchaseItem item : requestItemList) { + GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductName() + "|" + item.getUnit()); + TimiException.required(existingItem, "requestNo 对应购买请求内容不一致"); + TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0 && existingItem.getSort().equals(item.getSort()), "requestNo 对应购买请求内容不一致"); + } + } + + @Override + public Map> mapItemListByCustomerPackageIdList(Collection customerPackageIdList) { + if (customerPackageIdList == null || customerPackageIdList.isEmpty()) { + return Map.of(); + } + return itemMapper.selectByCustomerPackageIdList(customerPackageIdList).stream() + .collect(Collectors.groupingBy(GaoCustomerPackageItem::getCustomerPackageId)); + } + + private Long pendingAmount(GaoCustomerPackage packageEntity) { + return safeLong(packageEntity.getPendingPaidAmount()); + } + + private long safeLong(Long value) { + return value == null ? 0L : value; + } + + private String requiredReason(String reason, String message) { + TimiException.required(reason, message); + String result = reason.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java index 478f8d2..325112c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java @@ -160,6 +160,23 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList()); verifyRecordValue(event, record.getNumberValue()); @@ -232,6 +255,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = null; if (record.getSelectedOptionGroupList() != null) { eventService.attachOptionGroupList(List.of(event)); @@ -326,6 +351,8 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService implements GaoFundRecordService { + + private final GaoFundRecordMapper mapper; + private final GaoCustomerPackageService customerPackageService; + private final GaoPackageDeliveryService deliveryService; + private final AttachmentService attachmentService; + private final GaoStoreScopeService storeScopeService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + /// 资金流水必须通过专用操作创建 + /// + /// @param record 资金流水 + @Override + public void create(GaoFundRecord record) { + TimiException.requiredTrue(false, "资金流水必须通过专用接口创建"); + } + + /// 资金流水不允许通用修改 + /// + /// @param record 资金流水 + @Override + public void update(GaoFundRecord record) { + TimiException.requiredTrue(false, "资金流水不允许通用修改"); + } + + /// 资金流水不允许删除 + /// + /// @param id 资金流水 ID + @Override + public void delete(String id) { + TimiException.requiredTrue(false, "资金流水不允许删除"); + } + + /// 资金流水不允许物理删除 + /// + /// @param id 资金流水 ID + @Override + public void destroy(String id) { + TimiException.requiredTrue(false, "资金流水不允许物理删除"); + } + + @Override + public PageResult pageByQuery(GaoFundRecordPage page) { + TimiException.required(page, "not found page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + return result; + } + + @Override + public List listByCustomerPackageId(String customerPackageId) { + TimiException.required(customerPackageId, "not found customerPackageId"); + return mapper.selectByBizId(customerPackageId); + } + + @Override + public long countPendingReceiveByBizId(String bizId) { + return mapper.countPendingReceiveByBizId(bizId); + } + + @Override + public GaoFundRecord get(String id) { + return super.get(id); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord receive(GaoFundReceiveRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CREATE", request, () -> receiveInternal(request)); + } + + private GaoFundRecord receiveInternal(GaoFundReceiveRequest request) { + TimiException.required(request, "not found receive request"); + TimiException.required(request.getRequestNo(), "not found requestNo"); + request.setRequestNo(request.getRequestNo().trim()); + TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); + TimiException.required(request.getAmount(), "not found amount"); + TimiException.requiredTrue(0 < request.getAmount(), "收款金额必须大于 0"); + GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); + GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); + if (existing != null) { + TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) + && existing.getOperationType() == GaoFundRecord.OperationType.RECEIVE + && existing.getAmount().equals(request.getAmount()) + && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) + && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); + return get(existing.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "收款必须上传照片"); + checkPackageReceivable(packageEntity); + TimiException.requiredTrue(safeLong(packageEntity.getPaidAmount()) + safeLong(packageEntity.getPendingPaidAmount()) + request.getAmount() <= safeLong(packageEntity.getSaleAmount()), "累计收款不能超过成交价"); + GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.RECEIVE, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.PENDING); + super.create(record); + TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), request.getAmount()), "待核销收款更新失败"); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord verify(String fundRecordId) { + return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_VERIFY", fundRecordId, () -> verifyInternal(fundRecordId)); + } + + private GaoFundRecord verifyInternal(String fundRecordId) { + GaoFundRecord record = mapper.selectForUpdate(fundRecordId); + TimiException.required(record, "not found fund record"); + storeScopeService.checkStoreId(record.getStoreId()); + TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); + GaoCustomerPackage packageEntity = lockPackage(record.getBizId()); + TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败"); + TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), record.getAmount()), "有效收款更新失败"); + String userId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化"); + updatePaymentStatus(packageEntity, safeLong(packageEntity.getPaidAmount()) + record.getAmount(), safeLong(packageEntity.getPendingPaidAmount()) - record.getAmount()); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setVerifierUserId(userId); + record.setVerifiedAt(Time.now()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord reject(GaoFundActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_REJECT", request, () -> releasePending(request, GaoFundRecord.Status.REJECTED)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord cancel(GaoFundActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CANCEL", request, () -> { + TimiException.required(request, "not found fund action request"); + TimiException.required(request.getFundRecordId(), "not found fundRecordId"); + GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId()); + TimiException.required(record, "not found fund record"); + TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(record.getOperatorUserId()), "只能撤销自己提交的收款"); + return releasePending(request, GaoFundRecord.Status.CANCELLED); + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord refund(GaoFundRefundRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REFUND", request, () -> refundInternal(request)); + } + + private GaoFundRecord refundInternal(GaoFundRefundRequest request) { + TimiException.required(request, "not found refund request"); + TimiException.required(request.getRequestNo(), "not found requestNo"); + request.setRequestNo(request.getRequestNo().trim()); + TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); + TimiException.required(request.getAmount(), "not found amount"); + TimiException.requiredTrue(0 < request.getAmount(), "退款金额必须大于 0"); + TimiException.requiredTrue(hasReason(request.getReason()), "退款原因不能为空"); + GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); + GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); + if (existing != null) { + TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) + && existing.getOperationType() == GaoFundRecord.OperationType.REFUND + && existing.getAmount().equals(request.getAmount()) + && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) + && sameText(existing.getReason(), request.getReason().trim()) + && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); + return get(existing.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "退款必须上传照片"); + checkRefundable(packageEntity); + TimiException.requiredTrue(request.getAmount() <= safeLong(packageEntity.getPaidAmount()), "退款金额不能超过累计有效收款"); + GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.REFUND, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setReason(request.getReason().trim()); + super.create(record); + packageEntity.setRefundedAmount(request.getAmount()); + packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED); + customerPackageService.updateAggregateState(packageEntity); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_FUND_CONFIRM_NO_REFUND", request, () -> confirmNoRefundInternal(request)); + } + + private GaoFundRecord confirmNoRefundInternal(GaoFundConfirmNoRefundRequest request) { + TimiException.required(request, "not found no refund request"); + TimiException.required(request.getRequestNo(), "not found requestNo"); + request.setRequestNo(request.getRequestNo().trim()); + TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); + TimiException.requiredTrue(hasReason(request.getReason()), "确认无需退款原因不能为空"); + GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); + GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); + if (existing != null) { + TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) + && existing.getOperationType() == GaoFundRecord.OperationType.CONFIRM_NO_REFUND + && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) + && sameText(existing.getReason(), request.getReason().trim()) + && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); + return get(existing.getId()); + } + checkRefundable(packageEntity); + GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.CONFIRM_NO_REFUND, request.getRequestNo(), 0L, request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setReason(request.getReason().trim()); + super.create(record); + packageEntity.setRefundedAmount(0L); + packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED); + customerPackageService.updateAggregateState(packageEntity); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord revoke(GaoFundRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REVOKE", request, () -> revokeInternal(request)); + } + + private GaoFundRecord revokeInternal(GaoFundRevokeRequest request) { + TimiException.required(request, "not found fund revoke request"); + TimiException.required(request.getRequestNo(), "not found requestNo"); + request.setRequestNo(request.getRequestNo().trim()); + TimiException.required(request.getFundRecordId(), "not found fundRecordId"); + TimiException.requiredTrue(hasReason(request.getReason()), "资金冲正原因不能为空"); + GaoFundRecord source = mapper.selectForUpdate(request.getFundRecordId()); + TimiException.required(source, "not found fund record"); + storeScopeService.checkStoreId(source.getStoreId()); + GaoFundRecord existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo()); + if (existing != null) { + TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE + && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用"); + return get(existing.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "资金冲正必须上传照片"); + TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正"); + TimiException.requiredNull(mapper.selectRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正"); + GaoCustomerPackage packageEntity = lockPackage(source.getBizId()); + if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) { + TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "退款完成后不能冲正相关收款"); + long paidAfter = safeLong(packageEntity.getPaidAmount()) - source.getAmount(); + if (0 < deliveryService.countVerifiedByCustomerPackageId(packageEntity.getId())) { + TimiException.requiredTrue(packageEntity.getSaleAmount() <= paidAfter, "存在已核销发放,冲正收款后套餐仍须付清"); + } + TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), -source.getAmount()), "有效收款冲正失败"); + packageEntity.setPaidAmount(paidAfter); + updatePaymentStatus(packageEntity, paidAfter, packageEntity.getPendingPaidAmount()); + } else { + TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.REFUNDED, "资金记录退款状态无效"); + packageEntity.setRefundedAmount(0L); + packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.PENDING); + customerPackageService.updateAggregateState(packageEntity); + } + GaoFundRecord revoke = newRecord(packageEntity, GaoFundRecord.OperationType.REVOKE, request.getRequestNo(), source.getAmount(), request.getOccurredAt(), request.getRemark()); + revoke.setStatus(GaoFundRecord.Status.VERIFIED); + revoke.setSourceRecordId(source.getId()); + revoke.setReason(request.getReason().trim()); + super.create(revoke); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList()); + return revoke; + } + + private GaoFundRecord releasePending(GaoFundActionRequest request, GaoFundRecord.Status targetStatus) { + TimiException.required(request, "not found fund action request"); + TimiException.required(request.getFundRecordId(), "not found fundRecordId"); + TimiException.requiredTrue(hasReason(request.getReason()), "拒绝或撤销原因不能为空"); + GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId()); + TimiException.required(record, "not found fund record"); + storeScopeService.checkStoreId(record.getStoreId()); + TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); + GaoCustomerPackage packageEntity = lockPackage(record.getBizId()); + TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败"); + String userId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, targetStatus, userId, Time.now(), request.getReason().trim()), "收款记录状态已变化"); + record.setStatus(targetStatus); + record.setVerifierUserId(userId); + record.setVerifiedAt(Time.now()); + record.setReason(request.getReason().trim()); + return record; + } + + private GaoCustomerPackage lockPackage(String id) { + GaoCustomerPackage result = customerPackageService.getForUpdate(id); + TimiException.required(result, "not found customer package"); + storeScopeService.checkStoreId(result.getStoreId()); + return result; + } + + private void checkPackageReceivable(GaoCustomerPackage packageEntity) { + TimiException.requiredTrue(packageEntity.getStatus() != GaoCustomerPackage.Status.CANCELLED, "已取消套餐不能收款"); + TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "已完成退款套餐不能收款"); + } + + private void checkRefundable(GaoCustomerPackage packageEntity) { + TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.CANCELLED, "只有已取消套餐可以退款"); + TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.PENDING, "当前套餐不在待退款状态"); + } + + private GaoFundRecord newRecord(GaoCustomerPackage packageEntity, GaoFundRecord.OperationType operationType, String requestNo, Long amount, Long occurredAt, String remark) { + GaoFundRecord record = new GaoFundRecord(); + record.setStoreId(packageEntity.getStoreId()); + record.setCustomerId(packageEntity.getCustomerId()); + record.setBizType(GaoFundRecord.BizType.CUSTOMER_PACKAGE); + record.setBizId(packageEntity.getId()); + record.setOperationType(operationType); + record.setAmount(amount); + record.setRequestNo(requestNo); + record.setOperatorUserId(userLoginService.getRequireLoginUserId()); + record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now())); + record.setRemark(remark); + return record; + } + + private void updatePaymentStatus(GaoCustomerPackage packageEntity, long paidAmount, long pendingAmount) { + packageEntity.setPaidAmount(paidAmount); + packageEntity.setPendingPaidAmount(pendingAmount); + packageEntity.setPaymentStatus(safeLong(packageEntity.getSaleAmount()) == 0 || paidAmount >= safeLong(packageEntity.getSaleAmount()) + ? GaoCustomerPackage.PaymentStatus.PAID + : (0 < paidAmount ? GaoCustomerPackage.PaymentStatus.PARTIALLY_PAID : GaoCustomerPackage.PaymentStatus.UNPAID)); + customerPackageService.updateAggregateState(packageEntity); + } + + private boolean hasAttachment(List attachmentList) { + for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private boolean hasReason(String reason) { + return reason != null && !reason.trim().isEmpty(); + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } + + private long safeLong(Long value) { + return value == null ? 0L : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java new file mode 100644 index 0000000..77ec3f0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java @@ -0,0 +1,203 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; +import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper; +import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper; +import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/// GAO 套餐配置服务实现 +/// +/// @author Codex +/// @since 2026-08-31 +@Service +@RequiredArgsConstructor +public class GaoPackageConfigServiceImplement extends AbstractEntityService implements GaoPackageConfigService { + + private final GaoPackageConfigMapper mapper; + private final GaoPackageConfigItemMapper itemMapper; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoPackageConfigPage page) { + TimiException.required(page, "not found page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + return result; + } + + @Override + public List listValid(String storeId) { + TimiException.required(storeId, "not found storeId"); + return mapper.selectValidList(storeId, Time.now()); + } + + @Override + public List listItemByPackageConfigId(String packageConfigId) { + TimiException.required(packageConfigId, "not found packageConfigId"); + return itemMapper.selectByPackageConfigId(packageConfigId); + } + + @Override + public Map> mapItemListByPackageConfigIdList(Collection packageConfigIdList) { + if (packageConfigIdList == null || packageConfigIdList.isEmpty()) { + return Map.of(); + } + return itemMapper.selectByPackageConfigIdList(packageConfigIdList).stream() + .collect(java.util.stream.Collectors.groupingBy(GaoPackageConfigItem::getPackageConfigId)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoPackageConfig config) { + loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_CREATE", config, () -> { + createInternal(config); + return config; + }); + } + + private void createInternal(GaoPackageConfig config) { + TimiException.required(config, "not found package config"); + TimiException.required(config.getStoreId(), "not found packageConfig.storeId"); + TimiException.required(config.getItemList(), "套餐至少配置一个产品"); + prepareConfig(config, null); + config.setUpdatedBy(userLoginService.getRequireLoginUserId()); + super.create(config); + saveItems(config.getId(), config.getItemList()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoPackageConfig request) { + loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_UPDATE", request, () -> { + updateInternal(request); + return request; + }); + } + + private void updateInternal(GaoPackageConfig request) { + TimiException.required(request, "not found package config"); + TimiException.required(request.getId(), "not found packageConfig.id"); + GaoPackageConfig dbConfig = get(request.getId()); + TimiException.required(dbConfig, "not found package config"); + TimiException.requiredTrue(request.getStoreId() == null || request.getStoreId().equals(dbConfig.getStoreId()), "packageConfig.storeId invalid"); + dbConfig.setName(request.getName()); + dbConfig.setDescription(request.getDescription()); + dbConfig.setStandardAmount(TimiJava.defaultIfNull(request.getStandardAmount(), dbConfig.getStandardAmount())); + dbConfig.setStatus(TimiJava.defaultIfNull(request.getStatus(), dbConfig.getStatus())); + dbConfig.setBeginAt(request.getBeginAt()); + dbConfig.setEndAt(request.getEndAt()); + dbConfig.setSort(request.getSort()); + prepareConfig(dbConfig, dbConfig.getId()); + dbConfig.setUpdatedBy(userLoginService.getRequireLoginUserId()); + super.update(dbConfig); + if (request.getItemList() != null) { + verifyItems(request.getItemList()); + GaoPackageConfigItem example = new GaoPackageConfigItem(); + example.setPackageConfigId(dbConfig.getId()); + itemMapper.deleteAllByExample(example); + saveItems(dbConfig.getId(), request.getItemList()); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(String id) { + loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_DELETE", id, () -> { + GaoPackageConfig config = get(id); + TimiException.required(config, "not found package config"); + super.delete(id); + return id; + }); + } + + /// 套餐配置只允许逻辑删除 + /// + /// @param id 套餐配置 ID + @Override + public void destroy(String id) { + TimiException.requiredTrue(false, "套餐配置不允许物理删除"); + } + + private void prepareConfig(GaoPackageConfig config, String excludeId) { + String name = trimRequired(config.getName(), "套餐名称不能为空"); + TimiException.requiredNull(mapper.selectByStoreIdAndName(config.getStoreId(), name, excludeId), "门店内套餐名称不能重复"); + config.setName(name); + config.setStandardAmount(TimiJava.defaultIfNull(config.getStandardAmount(), 0L)); + TimiException.requiredTrue(0 <= config.getStandardAmount(), "套餐标准价不能为负数"); + config.setStatus(TimiJava.defaultIfNull(config.getStatus(), GaoPackageConfig.Status.DRAFT)); + config.setSort(TimiJava.defaultIfNull(config.getSort(), 0)); + TimiException.requiredTrue(config.getBeginAt() == null || config.getEndAt() == null || config.getBeginAt() <= config.getEndAt(), "套餐有效期无效"); + if (config.getItemList() != null) { + verifyItems(config.getItemList()); + } + } + + private void verifyItems(List itemList) { + TimiException.requiredTrue(!itemList.isEmpty(), "套餐至少配置一个产品"); + Set keySet = new HashSet<>(); + for (int i = 0; i < itemList.size(); i++) { + GaoPackageConfigItem item = itemList.get(i); + TimiException.required(item, "套餐产品明细不能为空"); + String name = trimRequired(item.getProductName(), "产品名称不能为空"); + String unit = trimRequired(item.getUnit(), "产品单位不能为空"); + BigDecimal quantity = item.getDefaultQuantity(); + TimiException.required(quantity, "产品默认数量不能为空"); + TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0"); + TimiException.requiredTrue(3 >= quantity.scale(), "产品数量最多保留三位小数"); + String key = name + "|" + unit; + TimiException.requiredTrue(keySet.add(key), "同一套餐内产品名称和单位不能重复"); + item.setProductName(name); + item.setUnit(unit); + item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); + } + } + + private void saveItems(String configId, List itemList) { + for (GaoPackageConfigItem source : TimiJava.safeIterable(itemList)) { + GaoPackageConfigItem item = new GaoPackageConfigItem(); + item.setPackageConfigId(configId); + item.setProductName(source.getProductName()); + item.setUnit(source.getUnit()); + item.setDefaultQuantity(source.getDefaultQuantity()); + item.setSort(source.getSort()); + item.setRemark(source.getRemark()); + itemMapper.insert(item); + } + } + + private String trimRequired(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageDeliveryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageDeliveryServiceImplement.java new file mode 100644 index 0000000..ee9370f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageDeliveryServiceImplement.java @@ -0,0 +1,410 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; +import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; +import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; +import com.imyeyu.api.modules.gao.mapper.GaoPackageDeliveryItemMapper; +import com.imyeyu.api.modules.gao.mapper.GaoPackageDeliveryMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; +import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; +import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/// GAO 套餐发放服务实现 +/// +/// @author Codex +/// @since 2026-08-31 +@Service +@RequiredArgsConstructor +public class GaoPackageDeliveryServiceImplement extends AbstractEntityService implements GaoPackageDeliveryService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoPackageDeliveryMapper mapper; + private final GaoPackageDeliveryItemMapper itemMapper; + private final GaoCustomerPackageService customerPackageService; + private final AttachmentService attachmentService; + private final GaoStoreScopeService storeScopeService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + /// 发放流水必须通过专用操作创建 + /// + /// @param delivery 发放流水 + @Override + public void create(GaoPackageDelivery delivery) { + throw new TimiException(TimiCode.ERROR, "发放流水必须通过专用接口创建"); + } + + /// 发放流水不允许通用修改 + /// + /// @param delivery 发放流水 + @Override + public void update(GaoPackageDelivery delivery) { + throw new TimiException(TimiCode.ERROR, "发放流水不允许通用修改"); + } + + /// 发放流水不允许删除 + /// + /// @param id 发放流水 ID + @Override + public void delete(String id) { + throw new TimiException(TimiCode.ERROR, "发放流水不允许删除"); + } + + /// 发放流水不允许物理删除 + /// + /// @param id 发放流水 ID + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "发放流水不允许物理删除"); + } + + @Override + public PageResult pageByQuery(GaoPackageDeliveryPage page) { + TimiException.required(page, "not found page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + return result; + } + + @Override + public List listByCustomerPackageId(String customerPackageId) { + TimiException.required(customerPackageId, "not found customerPackageId"); + return mapper.selectByCustomerPackageId(customerPackageId); + } + + @Override + public Map> mapItemListByDeliveryIdList(Collection deliveryIdList) { + if (deliveryIdList == null || deliveryIdList.isEmpty()) { + return Map.of(); + } + return itemMapper.selectByDeliveryIdList(deliveryIdList).stream() + .collect(java.util.stream.Collectors.groupingBy(GaoPackageDeliveryItem::getDeliveryId)); + } + + @Override + public long countPendingByCustomerPackageId(String customerPackageId) { + return mapper.countPendingByCustomerPackageId(customerPackageId); + } + + @Override + public long countVerifiedByCustomerPackageId(String customerPackageId) { + return mapper.countVerifiedByCustomerPackageId(customerPackageId); + } + + @Override + public GaoPackageDelivery get(String id) { + return super.get(id); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CREATE", request, () -> createDeliveryInternal(request)); + } + + private GaoPackageDelivery createDeliveryInternal(GaoPackageDeliveryCreateRequest request) { + TimiException.required(request, "not found delivery request"); + TimiException.required(request.getRequestNo(), "not found requestNo"); + request.setRequestNo(request.getRequestNo().trim()); + TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); + GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(request.getCustomerPackageId()); + TimiException.required(packageEntity, "not found customer package"); + storeScopeService.checkStoreId(packageEntity.getStoreId()); + GaoPackageDelivery existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); + if (existing != null) { + TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "requestNo 已被其他发放操作使用"); + verifyCreateIdempotency(existing, request); + return get(existing.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放必须上传照片"); + TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可发放"); + TimiException.requiredTrue(packageEntity.getPaymentStatus() == GaoCustomerPackage.PaymentStatus.PAID, "套餐未付清,不能发放"); + List requestItemList = validateRequestItems(request.getItemList()); + Map packageItemMap = new HashMap<>(); + for (GaoPackageDeliveryItem requestItem : requestItemList) { + GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(requestItem.getCustomerPackageItemId()); + TimiException.required(packageItem, "not found customer package item"); + TimiException.requiredTrue(packageEntity.getId().equals(packageItem.getCustomerPackageId()), "发放产品不属于当前套餐"); + packageItemMap.put(packageItem.getId(), packageItem); + } + + GaoPackageDelivery delivery = new GaoPackageDelivery(); + delivery.setCustomerPackageId(packageEntity.getId()); + delivery.setStoreId(packageEntity.getStoreId()); + delivery.setCustomerId(packageEntity.getCustomerId()); + delivery.setOperationType(GaoPackageDelivery.OperationType.DELIVER); + delivery.setStatus(GaoPackageDelivery.Status.PENDING); + delivery.setRequestNo(request.getRequestNo()); + delivery.setOperatorUserId(userLoginService.getRequireLoginUserId()); + delivery.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now())); + delivery.setRemark(request.getRemark()); + super.create(delivery); + for (GaoPackageDeliveryItem requestItem : requestItemList) { + GaoCustomerPackageItem packageItem = packageItemMap.get(requestItem.getCustomerPackageItemId()); + BigDecimal reservedBefore = decimal(packageItem.getReservedQuantity()); + BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity()); + BigDecimal available = decimal(packageItem.getTotalQuantity()).subtract(reservedBefore).subtract(deliveredBefore); + TimiException.requiredTrue(0 < requestItem.getQuantity().compareTo(available), "产品可发数量不足"); + int updated = customerPackageService.updateItemReservedQuantity(packageItem.getId(), requestItem.getQuantity()); + TimiException.requiredTrue(0 < updated, "产品可发数量不足"); + GaoPackageDeliveryItem savedItem = copyDeliveryItem(requestItem); + savedItem.setDeliveryId(delivery.getId()); + savedItem.setReservedBefore(reservedBefore); + savedItem.setReservedAfter(reservedBefore.add(requestItem.getQuantity())); + savedItem.setDeliveredBefore(deliveredBefore); + savedItem.setDeliveredAfter(deliveredBefore); + savedItem.setProductNameSnapshot(packageItem.getProductNameSnapshot()); + savedItem.setUnitSnapshot(packageItem.getUnitSnapshot()); + itemMapper.insert(savedItem); + } + attachmentService.updateByBizId(Attachment.BizType.GAO_PACKAGE_DELIVERY, delivery.getId(), request.getAttachmentList()); + return delivery; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPackageDelivery verify(String deliveryId) { + return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_VERIFY", deliveryId, () -> verifyInternal(deliveryId)); + } + + private GaoPackageDelivery verifyInternal(String deliveryId) { + GaoPackageDelivery delivery = mapper.selectForUpdate(deliveryId); + TimiException.required(delivery, "not found delivery"); + storeScopeService.checkStoreId(delivery.getStoreId()); + TimiException.requiredTrue(delivery.getStatus() == GaoPackageDelivery.Status.PENDING && delivery.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "发放单不是待核销状态"); + GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(delivery.getCustomerPackageId()); + TimiException.required(packageEntity, "not found customer package"); + TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可核销发放"); + TimiException.requiredTrue(packageEntity.getPaymentStatus() == GaoCustomerPackage.PaymentStatus.PAID, "套餐未付清,不能核销发放"); + List itemList = itemMapper.selectByDeliveryId(delivery.getId()); + for (GaoPackageDeliveryItem deliveryItem : itemList) { + GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(deliveryItem.getCustomerPackageItemId()); + TimiException.required(packageItem, "not found customer package item"); + TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(packageItem.getId(), deliveryItem.getQuantity().negate()), "发放占用数量异常"); + TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), deliveryItem.getQuantity()), "发放数量异常"); + } + TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, GaoPackageDelivery.Status.VERIFIED, userLoginService.getRequireLoginUserId(), Time.now(), delivery.getReason()), "发放单状态已变化"); + delivery.setStatus(GaoPackageDelivery.Status.VERIFIED); + delivery.setVerifierUserId(userLoginService.getRequireLoginUserId()); + delivery.setVerifiedAt(Time.now()); + refreshPackageStatus(packageEntity); + return delivery; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REJECT", request, () -> releasePending(request, GaoPackageDelivery.Status.REJECTED)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CANCEL", request, () -> cancelInternal(request)); + } + + private GaoPackageDelivery cancelInternal(GaoPackageDeliveryActionRequest request) { + TimiException.required(request, "not found delivery request"); + GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId()); + TimiException.required(delivery, "not found delivery"); + String loginUserId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(loginUserId.equals(delivery.getOperatorUserId()), "只能撤销自己提交的发放"); + return releasePending(request, GaoPackageDelivery.Status.CANCELLED); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REVOKE", request, () -> revokeInternal(request)); + } + + private GaoPackageDelivery revokeInternal(GaoPackageDeliveryRevokeRequest request) { + TimiException.required(request, "not found revoke request"); + TimiException.required(request.getRequestNo(), "not found requestNo"); + request.setRequestNo(request.getRequestNo().trim()); + TimiException.required(request.getDeliveryId(), "not found deliveryId"); + TimiException.required(hasReason(request.getReason()), "冲正原因不能为空"); + GaoPackageDelivery source = mapper.selectForUpdate(request.getDeliveryId()); + TimiException.required(source, "not found delivery"); + storeScopeService.checkStoreId(source.getStoreId()); + GaoPackageDelivery existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo()); + if (existing != null) { + TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.REVOKE + && source.getId().equals(existing.getSourceDeliveryId()), "requestNo 已被其他发放操作使用"); + return get(existing.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放冲正必须上传照片"); + TimiException.requiredTrue(source.getOperationType() == GaoPackageDelivery.OperationType.DELIVER && source.getStatus() == GaoPackageDelivery.Status.VERIFIED, "只有已核销发放可以冲正"); + TimiException.requiredNull(mapper.selectRevokeBySourceDeliveryId(source.getId()), "该发放单已经冲正"); + GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(source.getCustomerPackageId()); + TimiException.required(packageEntity, "not found customer package"); + List sourceItemList = itemMapper.selectByDeliveryId(source.getId()); + GaoPackageDelivery revoke = new GaoPackageDelivery(); + revoke.setCustomerPackageId(source.getCustomerPackageId()); + revoke.setStoreId(source.getStoreId()); + revoke.setCustomerId(source.getCustomerId()); + revoke.setOperationType(GaoPackageDelivery.OperationType.REVOKE); + revoke.setStatus(GaoPackageDelivery.Status.VERIFIED); + revoke.setRequestNo(request.getRequestNo()); + revoke.setOperatorUserId(userLoginService.getRequireLoginUserId()); + revoke.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now())); + revoke.setVerifierUserId(userLoginService.getRequireLoginUserId()); + revoke.setVerifiedAt(Time.now()); + revoke.setSourceDeliveryId(source.getId()); + revoke.setReason(request.getReason().trim()); + revoke.setRemark(request.getRemark()); + super.create(revoke); + for (GaoPackageDeliveryItem sourceItem : sourceItemList) { + GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(sourceItem.getCustomerPackageItemId()); + TimiException.required(packageItem, "not found customer package item"); + BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity()); + TimiException.requiredTrue(0 < deliveredBefore.compareTo(sourceItem.getQuantity()) || deliveredBefore.compareTo(sourceItem.getQuantity()) == 0, "已发数量不足以冲正"); + TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), sourceItem.getQuantity().negate()), "发放数量冲正失败"); + GaoPackageDeliveryItem revokeItem = copyDeliveryItem(sourceItem); + revokeItem.setDeliveryId(revoke.getId()); + revokeItem.setReservedBefore(packageItem.getReservedQuantity()); + revokeItem.setReservedAfter(packageItem.getReservedQuantity()); + revokeItem.setDeliveredBefore(deliveredBefore); + revokeItem.setDeliveredAfter(deliveredBefore.subtract(sourceItem.getQuantity())); + itemMapper.insert(revokeItem); + } + TimiException.requiredTrue(0 < mapper.updateStatusIf(source.getId(), GaoPackageDelivery.Status.VERIFIED, GaoPackageDelivery.Status.REVOKED, source.getVerifierUserId(), source.getVerifiedAt(), source.getReason()), "发放单状态已变化"); + if (packageEntity.getStatus() == GaoCustomerPackage.Status.FULFILLED) { + packageEntity.setStatus(GaoCustomerPackage.Status.ACTIVE); + customerPackageService.updateAggregateState(packageEntity); + } + attachmentService.updateByBizId(Attachment.BizType.GAO_PACKAGE_DELIVERY, revoke.getId(), request.getAttachmentList()); + return revoke; + } + + private GaoPackageDelivery releasePending(GaoPackageDeliveryActionRequest request, GaoPackageDelivery.Status targetStatus) { + TimiException.required(request, "not found delivery request"); + TimiException.required(request.getDeliveryId(), "not found deliveryId"); + TimiException.required(hasReason(request.getReason()), "拒绝或撤销原因不能为空"); + GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId()); + TimiException.required(delivery, "not found delivery"); + storeScopeService.checkStoreId(delivery.getStoreId()); + TimiException.requiredTrue(delivery.getStatus() == GaoPackageDelivery.Status.PENDING && delivery.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "发放单不是待核销状态"); + for (GaoPackageDeliveryItem item : itemMapper.selectByDeliveryId(delivery.getId())) { + TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(item.getCustomerPackageItemId(), item.getQuantity().negate()), "发放占用数量异常"); + } + String loginUserId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, targetStatus, loginUserId, Time.now(), request.getReason().trim()), "发放单状态已变化"); + delivery.setStatus(targetStatus); + delivery.setVerifierUserId(loginUserId); + delivery.setVerifiedAt(Time.now()); + delivery.setReason(request.getReason().trim()); + return delivery; + } + + private void refreshPackageStatus(GaoCustomerPackage packageEntity) { + List itemList = customerPackageService.listItemByCustomerPackageId(packageEntity.getId()); + boolean fulfilled = !itemList.isEmpty(); + for (GaoCustomerPackageItem item : itemList) { + if (decimal(item.getDeliveredQuantity()).compareTo(decimal(item.getTotalQuantity())) < 0) { + fulfilled = false; + break; + } + } + if (fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) { + packageEntity.setStatus(GaoCustomerPackage.Status.FULFILLED); + customerPackageService.updateAggregateState(packageEntity); + } + } + + private List validateRequestItems(List sourceList) { + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "发放至少需要一个产品"); + Set idSet = new HashSet<>(); + List result = new ArrayList<>(); + for (GaoPackageDeliveryItem source : sourceList) { + TimiException.required(source, "发放产品明细不能为空"); + TimiException.required(source.getCustomerPackageItemId(), "not found customerPackageItemId"); + TimiException.required(source.getQuantity(), "发放数量不能为空"); + TimiException.requiredTrue(0 < source.getQuantity().signum(), "发放数量必须大于 0"); + TimiException.requiredTrue(3 >= source.getQuantity().scale(), "发放数量最多保留三位小数"); + TimiException.requiredTrue(idSet.add(source.getCustomerPackageItemId()), "同一发放单产品不能重复"); + result.add(source); + } + result.sort(Comparator.comparing(GaoPackageDeliveryItem::getCustomerPackageItemId)); + return result; + } + + private GaoPackageDeliveryItem copyDeliveryItem(GaoPackageDeliveryItem source) { + GaoPackageDeliveryItem result = new GaoPackageDeliveryItem(); + result.setCustomerPackageItemId(source.getCustomerPackageItemId()); + result.setQuantity(source.getQuantity()); + return result; + } + + private void verifyCreateIdempotency(GaoPackageDelivery existing, GaoPackageDeliveryCreateRequest request) { + TimiException.requiredTrue(existing.getCustomerPackageId().equals(request.getCustomerPackageId()) + && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) + && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应请求内容不一致"); + List existingItems = itemMapper.selectByDeliveryId(existing.getId()); + List requestItems = validateRequestItems(request.getItemList()); + TimiException.requiredTrue(existingItems.size() == requestItems.size(), "requestNo 对应请求内容不一致"); + Map requestMap = requestItems.stream().collect(java.util.stream.Collectors.toMap(GaoPackageDeliveryItem::getCustomerPackageItemId, GaoPackageDeliveryItem::getQuantity)); + for (GaoPackageDeliveryItem item : existingItems) { + TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getCustomerPackageItemId(), ZERO)) == 0, "requestNo 对应请求内容不一致"); + } + } + + private boolean hasAttachment(List attachmentList) { + for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private boolean hasReason(String reason) { + return reason != null && !reason.trim().isEmpty(); + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } + + private BigDecimal decimal(BigDecimal value) { + return value == null ? ZERO : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackageCancelRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackageCancelRequest.java new file mode 100644 index 0000000..7260ac5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackageCancelRequest.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 取消客户套餐请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoCustomerPackageCancelRequest { + + /// 客户套餐 ID + private String customerPackageId; + + /// 取消原因 + private String reason; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java new file mode 100644 index 0000000..eab3cb6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java @@ -0,0 +1,39 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 客户套餐分页查询请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoCustomerPackagePage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 客户或套餐关键词 + private String keyword; + + /// 套餐状态 + private GaoCustomerPackage.Status status; + + /// 付款状态 + private GaoCustomerPackage.PaymentStatus paymentStatus; + + /// 退款状态 + private GaoCustomerPackage.RefundStatus refundStatus; + + /// 购买开始时间 + private Long beginAt; + + /// 购买结束时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java new file mode 100644 index 0000000..bd877a5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// GAO 客户套餐购买产品明细 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoCustomerPackagePurchaseItem { + + /// 产品名称 + private String productName; + + /// 产品单位 + private String unit; + + /// 总权益数量 + private BigDecimal totalQuantity; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java new file mode 100644 index 0000000..15a8c3e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java @@ -0,0 +1,41 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 客户购买套餐请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoCustomerPackagePurchaseRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 购买事件 ID + private String eventId; + + /// 客户 ID + private String customerId; + + /// 套餐配置 ID + private String packageConfigId; + + /// 实际成交价,单位分 + private Long saleAmount; + + /// 购买时间 + private Long purchasedAt; + + /// 最终产品明细 + private List itemList; + + /// 购买照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java new file mode 100644 index 0000000..4ca1626 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 资金收款状态操作请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoFundActionRequest { + + /// 资金记录 ID + private String fundRecordId; + + /// 拒绝或撤销原因 + private String reason; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java new file mode 100644 index 0000000..786ce1b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 确认无需退款请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoFundConfirmNoRefundRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 客户套餐 ID + private String customerPackageId; + + /// 确认时间 + private Long occurredAt; + + /// 确认原因 + private String reason; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java new file mode 100644 index 0000000..aedb2e8 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 线下收款登记请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoFundReceiveRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 客户套餐 ID + private String customerPackageId; + + /// 收款金额,单位分 + private Long amount; + + /// 收款发生时间 + private Long occurredAt; + + /// 收款照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java new file mode 100644 index 0000000..ec0d9f2 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 资金流水分页查询请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoFundRecordPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 客户套餐 ID + private String customerPackageId; + + /// 客户 ID + private String customerId; + + /// 操作类型 + private GaoFundRecord.OperationType operationType; + + /// 操作状态 + private GaoFundRecord.Status status; + + /// 操作开始时间 + private Long beginAt; + + /// 操作结束时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java new file mode 100644 index 0000000..81dbb35 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 退款登记请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoFundRefundRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 客户套餐 ID + private String customerPackageId; + + /// 退款金额,单位分 + private Long amount; + + /// 退款发生时间 + private Long occurredAt; + + /// 退款原因 + private String reason; + + /// 退款照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRevokeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRevokeRequest.java new file mode 100644 index 0000000..2cdfb8e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRevokeRequest.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 资金流水冲正请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoFundRevokeRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 原资金记录 ID + private String fundRecordId; + + /// 冲正发生时间 + private Long occurredAt; + + /// 冲正原因 + private String reason; + + /// 冲正照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageConfigPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageConfigPage.java new file mode 100644 index 0000000..715593a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageConfigPage.java @@ -0,0 +1,24 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 套餐配置分页查询请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPackageConfigPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 套餐名称关键词 + private String keyword; + + /// 套餐状态 + private GaoPackageConfig.Status status; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java new file mode 100644 index 0000000..dfd2e7c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 套餐发放状态操作请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoPackageDeliveryActionRequest { + + /// 发放单 ID + private String deliveryId; + + /// 拒绝或撤销原因 + private String reason; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java new file mode 100644 index 0000000..4eede73 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; +import lombok.Data; + +import java.util.List; + +/// GAO 套餐发放请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoPackageDeliveryCreateRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 客户套餐 ID + private String customerPackageId; + + /// 实际发放时间 + private Long deliveredAt; + + /// 发放产品明细 + private List itemList; + + /// 发放照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java new file mode 100644 index 0000000..73c788e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 套餐发放分页查询请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPackageDeliveryPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 客户套餐 ID + private String customerPackageId; + + /// 客户 ID + private String customerId; + + /// 发放状态 + private GaoPackageDelivery.Status status; + + /// 操作类型 + private GaoPackageDelivery.OperationType operationType; + + /// 发放开始时间 + private Long beginAt; + + /// 发放结束时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java new file mode 100644 index 0000000..ec9e207 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 已核销发放整单冲正请求 +/// +/// @author Codex +/// @since 2026-08-31 +@Data +public class GaoPackageDeliveryRevokeRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 原发放单 ID + private String deliveryId; + + /// 冲正发生时间 + private Long deliveredAt; + + /// 冲正原因 + private String reason; + + /// 冲正照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java b/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java index 45e4463..39f8afa 100644 --- a/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java +++ b/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java @@ -6,6 +6,8 @@ import com.imyeyu.spring.service.DestroyableService; import com.imyeyu.spring.service.GettableService; import com.imyeyu.spring.service.PageableService; +import java.util.function.Supplier; + /** * 日志服务 * @@ -16,6 +18,51 @@ public interface LoggerService extends GettableService, Pageable void createSync(Logger logger); + /// 执行并记录有返回值的业务操作 + /// + /// @param logger 日志对象,调用方负责设置模块、操作和请求内容 + /// @param action 业务操作 + /// @param 返回值类型 + /// @return 业务操作返回值 + T execute(Logger logger, Supplier action); + + /// 执行并记录无返回值的业务操作 + /// + /// @param logger 日志对象,调用方负责设置模块、操作和请求内容 + /// @param action 业务操作 + default void execute(Logger logger, Runnable action) { + execute(logger, () -> { + action.run(); + return null; + }); + } + + /// 创建日志并执行有返回值的业务操作 + /// + /// @param module 日志模块 + /// @param operation 操作描述 + /// @param content 请求内容 + /// @param action 业务操作 + /// @param 返回值类型 + /// @return 业务操作返回值 + default T execute(Logger.Module module, String operation, Object content, Supplier action) { + Logger logger = new Logger(module, operation); + logger.setContent(content == null ? null : content.toString()); + return execute(logger, action); + } + + /// 创建日志并执行无返回值的业务操作 + /// + /// @param module 日志模块 + /// @param operation 操作描述 + /// @param content 请求内容 + /// @param action 业务操作 + default void execute(Logger.Module module, String operation, Object content, Runnable action) { + Logger logger = new Logger(module, operation); + logger.setContent(content == null ? null : content.toString()); + execute(logger, action); + } + void create(Logger.Module module, Logger.Level level, String operation, String content, String result); default void info(Logger.Module module, String operation) { diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java index af0a835..dff254a 100644 --- a/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java @@ -6,13 +6,17 @@ import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.entity.IDEntity; import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.function.Supplier; + /** * 日志服务实现 * @@ -46,6 +50,32 @@ public class LoggerServiceImplement extends AbstractEntityService T execute(Logger logger, Supplier action) { + try { + T result = action.get(); + logger.setLevel(Logger.Level.INFO); + if (result instanceof IDEntity entity) { + logger.setResult(String.valueOf(entity.getId())); + } else if (result != null) { + logger.setResult(result.toString()); + } + create(logger); + return result; + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + create(logger); + throw e; + } catch (RuntimeException e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("业务操作失败: {}", logger.getOperation(), e); + create(logger); + throw e; + } + } + public void create(Logger.Module module, Logger.Level level, String operation, String content, String result) { Logger log = new Logger(module, level, operation); log.setContent(content); diff --git a/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java b/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java index 4e1c281..b77d8df 100644 --- a/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java +++ b/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java @@ -206,7 +206,6 @@ public class InitBuiltinUser implements ApplicationRunner { private void bindBuiltinRoleParent(Map roleMap) { setRoleParent(roleMap, ModuleCode.CORE, CoreRoleCode.ADMIN.name(), ModuleCode.CORE, CoreRoleCode.SYSTEM.name()); setRoleParent(roleMap, ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name(), ModuleCode.CORE, CoreRoleCode.ADMIN.name()); - setRoleParent(roleMap, ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name(), ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name()); } /// 按权限定义补齐 SYSTEM 和 ADMIN 的默认权限 diff --git a/src/main/resources/db/migration/timiserver/V48__create_gao_package_tables.sql b/src/main/resources/db/migration/timiserver/V48__create_gao_package_tables.sql new file mode 100644 index 0000000..78474b2 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V48__create_gao_package_tables.sql @@ -0,0 +1,157 @@ +ALTER TABLE `gao_event` + ADD COLUMN `biz_type` VARCHAR(32) NOT NULL DEFAULT 'GENERAL' COMMENT '业务类型,GENERAL 普通登记,PACKAGE_PURCHASE 购买产品套餐' AFTER `id`; + +CREATE TABLE IF NOT EXISTS `gao_package_config` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID', + `name` VARCHAR(128) NOT NULL COMMENT '套餐名称', + `description` VARCHAR(255) NULL COMMENT '套餐说明', + `standard_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '标准总价,单位分', + `status` VARCHAR(32) NOT NULL COMMENT 'DRAFT 草稿,ACTIVE 启用,INACTIVE 停用', + `begin_at` BIGINT NULL COMMENT '有效期开始时间戳,毫秒', + `end_at` BIGINT NULL COMMENT '有效期结束时间戳,毫秒', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `updated_by` VARCHAR(36) NOT NULL COMMENT '最后修改用户 ID', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_package_config_store_status` (`store_id`, `status`, `deleted_at`, `begin_at`, `end_at`, `sort`) +) COMMENT='GAO 套餐配置'; + +CREATE TABLE IF NOT EXISTS `gao_package_config_item` ( + `id` VARCHAR(36) NOT NULL, + `package_config_id` VARCHAR(36) NOT NULL COMMENT '套餐配置 ID', + `product_name` VARCHAR(128) NOT NULL COMMENT '产品名称', + `unit` VARCHAR(32) NOT NULL COMMENT '产品单位', + `default_quantity` DECIMAL(18,3) NOT NULL COMMENT '默认数量', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `remark` VARCHAR(255) NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_package_config_item_config` (`package_config_id`, `deleted_at`, `sort`, `created_at`) +) COMMENT='GAO 套餐配置产品明细'; + +CREATE TABLE IF NOT EXISTS `gao_customer_package` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `package_config_id` VARCHAR(36) NOT NULL COMMENT '来源套餐配置 ID', + `purchase_event_record_id` VARCHAR(36) NOT NULL COMMENT '购买事件登记 ID', + `request_no` VARCHAR(128) NOT NULL COMMENT '购买幂等请求号', + `package_name_snapshot` VARCHAR(128) NOT NULL COMMENT '套餐名称快照', + `standard_amount_snapshot` BIGINT NOT NULL COMMENT '标准价快照,单位分', + `sale_amount` BIGINT NOT NULL COMMENT '实际成交价,单位分', + `pending_paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '待核销收款合计,单位分', + `paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '累计有效收款,单位分', + `refunded_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '累计有效退款,单位分', + `status` VARCHAR(32) NOT NULL COMMENT '套餐状态', + `payment_status` VARCHAR(32) NOT NULL COMMENT '付款状态', + `refund_status` VARCHAR(32) NOT NULL COMMENT '退款状态', + `purchased_at` BIGINT NOT NULL COMMENT '购买时间', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '购买登记人', + `cancelled_at` BIGINT NULL COMMENT '取消时间', + `cancel_operator_user_id` VARCHAR(36) NULL COMMENT '取消操作人', + `cancel_reason` TEXT NULL COMMENT '取消原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_customer_package_purchase_event` (`purchase_event_record_id`), + UNIQUE KEY `uk_gao_customer_package_request` (`store_id`, `request_no`), + KEY `idx_gao_customer_package_customer` (`store_id`, `customer_id`, `status`, `deleted_at`, `purchased_at`), + KEY `idx_gao_customer_package_payment` (`store_id`, `payment_status`, `refund_status`, `deleted_at`, `purchased_at`) +) COMMENT='GAO 客户套餐'; + +CREATE TABLE IF NOT EXISTS `gao_customer_package_item` ( + `id` VARCHAR(36) NOT NULL, + `customer_package_id` VARCHAR(36) NOT NULL COMMENT '客户套餐 ID', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '产品单位快照', + `total_quantity` DECIMAL(18,3) NOT NULL COMMENT '总权益数量', + `reserved_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '待核销占用数量', + `delivered_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已核销净发放数量', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `remark` VARCHAR(255) NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_customer_package_item_package` (`customer_package_id`, `deleted_at`, `sort`, `created_at`) +) COMMENT='GAO 客户套餐产品权益快照'; + +CREATE TABLE IF NOT EXISTS `gao_package_delivery` ( + `id` VARCHAR(36) NOT NULL, + `customer_package_id` VARCHAR(36) NOT NULL COMMENT '客户套餐 ID', + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `operation_type` VARCHAR(32) NOT NULL COMMENT 'DELIVER 正常发放,REVOKE 整单冲正', + `status` VARCHAR(32) NOT NULL COMMENT '发放状态', + `request_no` VARCHAR(128) NOT NULL COMMENT '客户端幂等请求号', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '提交人', + `delivered_at` BIGINT NOT NULL COMMENT '实际发放时间', + `verifier_user_id` VARCHAR(36) NULL COMMENT '核销或拒绝操作人', + `verified_at` BIGINT NULL COMMENT '核销或拒绝时间', + `source_delivery_id` VARCHAR(36) NULL COMMENT '冲正单指向原发放单', + `reason` TEXT NULL COMMENT '拒绝、撤销或冲正原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_package_delivery_request` (`store_id`, `request_no`), + UNIQUE KEY `uk_gao_package_delivery_revoke_source` (`source_delivery_id`), + KEY `idx_gao_package_delivery_package_time` (`customer_package_id`, `delivered_at`, `id`), + KEY `idx_gao_package_delivery_customer_time` (`store_id`, `customer_id`, `delivered_at`, `id`), + KEY `idx_gao_package_delivery_status` (`store_id`, `status`, `delivered_at`, `id`) +) COMMENT='GAO 套餐发放操作'; + +CREATE TABLE IF NOT EXISTS `gao_package_delivery_item` ( + `id` VARCHAR(36) NOT NULL, + `delivery_id` VARCHAR(36) NOT NULL COMMENT '发放单 ID', + `customer_package_item_id` VARCHAR(36) NOT NULL COMMENT '客户套餐产品 ID', + `quantity` DECIMAL(18,3) NOT NULL COMMENT '本次数量', + `reserved_before` DECIMAL(18,3) NOT NULL COMMENT '操作前占用数量快照', + `reserved_after` DECIMAL(18,3) NOT NULL COMMENT '操作后占用数量快照', + `delivered_before` DECIMAL(18,3) NOT NULL COMMENT '操作前已发数量快照', + `delivered_after` DECIMAL(18,3) NOT NULL COMMENT '操作后已发数量快照', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '产品单位快照', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_package_delivery_item` (`delivery_id`, `customer_package_item_id`), + KEY `idx_gao_package_delivery_item_package_item` (`customer_package_item_id`, `created_at`, `id`) +) COMMENT='GAO 套餐发放产品明细'; + +CREATE TABLE IF NOT EXISTS `gao_fund_record` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NULL COMMENT '客户 ID', + `biz_type` VARCHAR(32) NOT NULL COMMENT '业务类型', + `biz_id` VARCHAR(36) NOT NULL COMMENT '业务对象 ID', + `operation_type` VARCHAR(32) NOT NULL COMMENT '资金操作类型', + `status` VARCHAR(32) NOT NULL COMMENT '资金操作状态', + `amount` BIGINT NOT NULL COMMENT '金额,单位分', + `source_record_id` VARCHAR(36) NULL COMMENT '冲正时关联原资金记录', + `request_no` VARCHAR(128) NOT NULL COMMENT '幂等请求号', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '提交操作用户 ID', + `verifier_user_id` VARCHAR(36) NULL COMMENT '收款核销用户 ID', + `verified_at` BIGINT NULL COMMENT '收款核销时间', + `occurred_at` BIGINT NOT NULL COMMENT '线下操作发生时间', + `reason` TEXT NULL COMMENT '原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_fund_record_request` (`store_id`, `request_no`), + UNIQUE KEY `uk_gao_fund_record_revoke_source` (`source_record_id`), + KEY `idx_gao_fund_record_biz_time` (`store_id`, `biz_type`, `biz_id`, `occurred_at`, `id`), + KEY `idx_gao_fund_record_customer_time` (`store_id`, `customer_id`, `occurred_at`, `id`), + KEY `idx_gao_fund_record_status` (`store_id`, `status`, `operation_type`, `occurred_at`, `id`) +) COMMENT='GAO 通用资金操作流水'; diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml new file mode 100644 index 0000000..606831d --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + UPDATE `gao_customer_package_item` + SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL AND `reserved_quantity` + #{delta} >= 0 + + + UPDATE `gao_customer_package_item` + SET `delivered_quantity` = `delivered_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL AND `delivered_quantity` + #{delta} >= 0 + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml new file mode 100644 index 0000000..d6696fc --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + p.`deleted_at` IS NULL + AND p.`store_id` = #{storeId} + AND p.`customer_id` = #{customerId} + AND p.`status` = #{status} + AND p.`payment_status` = #{paymentStatus} + AND p.`refund_status` = #{refundStatus} + AND p.`purchased_at` >= #{beginAt} + AND p.`purchased_at` <= #{endAt} + AND (p.`package_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR c.`name` LIKE CONCAT('%', #{keyword}, '%') OR c.`code` LIKE CONCAT('%', #{keyword}, '%')) + + + + + UPDATE `gao_customer_package` + SET `pending_paid_amount` = `pending_paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL AND `pending_paid_amount` + #{delta} >= 0 + + + UPDATE `gao_customer_package` + SET `paid_amount` = `paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL AND `paid_amount` + #{delta} >= 0 + + + UPDATE `gao_customer_package` + SET `status` = #{toStatus}, `cancelled_at` = #{cancelledAt}, `cancel_operator_user_id` = #{cancelOperatorUserId}, `cancel_reason` = #{cancelReason}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml index f8b7fd9..3f315ae 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml @@ -136,6 +136,9 @@ AND ( c.`code` LIKE CONCAT('%', #{keyword}, '%') OR c.`name` LIKE CONCAT('%', #{keyword}, '%') + OR c.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%') + OR c.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') + OR c.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%') OR r.`remark` LIKE CONCAT('%', #{keyword}, '%') OR r.`operator_user_id` LIKE CONCAT('%', #{keyword}, '%') ) diff --git a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml new file mode 100644 index 0000000..9b445d6 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + f.`deleted_at` IS NULL + AND f.`store_id` = #{storeId} + AND f.`biz_id` = #{customerPackageId} + AND f.`customer_id` = #{customerId} + AND f.`operation_type` = #{operationType} + AND f.`status` = #{status} + AND f.`occurred_at` >= #{beginAt} + AND f.`occurred_at` <= #{endAt} + + + + + UPDATE `gao_fund_record` + SET `status` = #{toStatus}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageConfigItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigItemMapper.xml new file mode 100644 index 0000000..1bb3f37 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigItemMapper.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageConfigMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigMapper.xml new file mode 100644 index 0000000..3df1444 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml new file mode 100644 index 0000000..3c723c7 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml new file mode 100644 index 0000000..3f2c6c4 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + d.`deleted_at` IS NULL + AND d.`store_id` = #{storeId} + AND d.`customer_package_id` = #{customerPackageId} + AND d.`customer_id` = #{customerId} + AND d.`status` = #{status} + AND d.`operation_type` = #{operationType} + AND d.`delivered_at` >= #{beginAt} + AND d.`delivered_at` <= #{endAt} + + + + + UPDATE `gao_package_delivery` + SET `status` = #{toStatus}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL + +