v1.0.31 #43

Merged
timi merged 10 commits from dev into master 2026-09-07 07:40:03 +00:00
129 changed files with 6298 additions and 1719 deletions
Showing only changes of commit 731c777b8b - Show all commits
@@ -38,12 +38,16 @@ public class Attachment extends UUIDEntity {
GAO_CUSTOMER, GAO_CUSTOMER,
GAO_PRODUCT,
GAO_EVENT_RECORD, GAO_EVENT_RECORD,
GAO_PACKAGE_DELIVERY,
GAO_FUND_RECORD, GAO_FUND_RECORD,
GAO_STOCK_RECORD,
GAO_ORDER_FULFILLMENT,
TEMP_FILE, TEMP_FILE,
MIRROR MIRROR
@@ -3,6 +3,8 @@ package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Article; import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
import java.util.List;
/** /**
* 文章服务 * 文章服务
* *
@@ -10,4 +12,10 @@ import com.imyeyu.spring.service.BaseService;
* @since 2021-02-23 21:33 * @since 2021-02-23 21:33
*/ */
public interface ArticleService extends BaseService<Article, String> { public interface ArticleService extends BaseService<Article, String> {
/// 按示例查询文章
///
/// @param example 查询示例
/// @return 文章列表
List<Article> listByExample(Article example);
} }
@@ -16,6 +16,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* 文章服务实现 * 文章服务实现
* *
@@ -37,6 +39,11 @@ public class ArticleServiceImplement extends AbstractEntityService<Article, Stri
return mapper; return mapper;
} }
@Override
public List<Article> listByExample(Article example) {
return mapper.selectAllByExample(example);
}
@Override @Override
public void create(Article article) { public void create(Article article) {
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_ARTICLE_CREATE"); Logger logger = new Logger(Logger.Module.COMMON, "COMMON_ARTICLE_CREATE");
@@ -71,6 +71,7 @@ public class TempFileServiceImplement implements TempFileService {
page.setSize(Long.MAX_VALUE); page.setSize(Long.MAX_VALUE);
{ {
Attachment example = new Attachment(); Attachment example = new Attachment();
example.setBizType(BIZ_TYPE);
example.setUploaderIp(TimiSpring.getRequestIP()); example.setUploaderIp(TimiSpring.getRequestIP());
page.setEqualsExample(example); page.setEqualsExample(example);
} }
@@ -59,6 +59,38 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false), EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false),
PRODUCT_CATEGORY_CREATE("PRODUCT_CATEGORY:CREATE", "产品分类创建", true, true),
PRODUCT_CATEGORY_READ("PRODUCT_CATEGORY:READ", "产品分类读取", true, true),
PRODUCT_CATEGORY_UPDATE("PRODUCT_CATEGORY:UPDATE", "产品分类修改", true, true),
PRODUCT_CREATE("PRODUCT:CREATE", "产品创建", true, true),
PRODUCT_READ("PRODUCT:READ", "产品读取", true, true),
PRODUCT_UPDATE("PRODUCT:UPDATE", "产品修改", true, true),
PRODUCT_STOCK_READ("PRODUCT_STOCK:READ", "产品库存读取", true, true),
PRODUCT_STOCK_CREATE("PRODUCT_STOCK:CREATE", "产品库存操作", true, true),
SALES_ORDER_CREATE("SALES_ORDER:CREATE", "销售订单创建", true, true),
SALES_ORDER_READ("SALES_ORDER:READ", "销售订单读取", true, true),
SALES_ORDER_CANCEL("SALES_ORDER:CANCEL", "销售订单取消", true, true),
SALES_ORDER_PRICE_OVERRIDE("SALES_ORDER:PRICE_OVERRIDE", "销售订单价格修改", true, true),
ORDER_FULFILLMENT_CREATE("ORDER_FULFILLMENT:CREATE", "统一履约创建", true, true),
ORDER_FULFILLMENT_READ("ORDER_FULFILLMENT:READ", "统一履约读取", true, true),
ORDER_FULFILLMENT_VERIFY("ORDER_FULFILLMENT:VERIFY", "统一履约核销", true, true),
ORDER_FULFILLMENT_REVOKE("ORDER_FULFILLMENT:REVOKE", "统一履约冲正", true, true),
PACKAGE_CONFIG_CREATE("PACKAGE_CONFIG:CREATE", "套餐配置创建", true, true), PACKAGE_CONFIG_CREATE("PACKAGE_CONFIG:CREATE", "套餐配置创建", true, true),
PACKAGE_CONFIG_READ("PACKAGE_CONFIG:READ", "套餐配置读取", true, true), PACKAGE_CONFIG_READ("PACKAGE_CONFIG:READ", "套餐配置读取", true, true),
@@ -7,23 +7,14 @@ import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
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.GaoCustomerPackageService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
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.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; 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.entity.User;
import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
@@ -40,9 +31,8 @@ import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream;
/// GAO 客户套餐和收退款接口 /// GAO 客户套餐接口
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-08-31
@@ -53,9 +43,7 @@ public class GaoCustomerPackageController {
private final GaoCustomerPackageService service; private final GaoCustomerPackageService service;
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final GaoEventRecordService eventRecordService; private final GaoOrderFulfillmentService fulfillmentService;
private final GaoPackageDeliveryService deliveryService;
private final GaoFundRecordService fundRecordService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final UserService userService; private final UserService userService;
@@ -102,15 +90,7 @@ public class GaoCustomerPackageController {
customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId()));
} }
result.setCustomer(customer); result.setCustomer(customer);
GaoEventRecord purchaseRecord = eventRecordService.get(result.getPurchaseEventRecordId()); result.setFulfillmentList(fulfillmentService.listBySalesOrderId(result.getSalesOrderId()));
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; return result;
} }
@@ -134,80 +114,13 @@ public class GaoCustomerPackageController {
return 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<GaoCustomerPackage> packageList) { private void fillTransient(List<GaoCustomerPackage> packageList) {
if (packageList == null || packageList.isEmpty()) { if (packageList == null || packageList.isEmpty()) {
return; return;
} }
Map<String, List<GaoCustomerPackageItem>> itemMap = service.mapItemListByCustomerPackageIdList(packageList.stream().map(GaoCustomerPackage::getId).toList()); Map<String, List<GaoCustomerPackageItem>> itemMap = service.mapItemListByCustomerPackageIdList(packageList.stream().map(GaoCustomerPackage::getId).toList());
List<String> userIdList = packageList.stream() List<String> userIdList = packageList.stream()
.flatMap(packageEntity -> Stream.of(packageEntity.getOperatorUserId(), packageEntity.getCancelOperatorUserId())) .map(GaoCustomerPackage::getCancelOperatorUserId)
.filter(value -> value != null && !value.isBlank()) .filter(value -> value != null && !value.isBlank())
.distinct() .distinct()
.toList(); .toList();
@@ -218,57 +131,16 @@ public class GaoCustomerPackageController {
fillDerived(item); fillDerived(item);
} }
packageEntity.setItemList(itemList); packageEntity.setItemList(itemList);
packageEntity.setOperator(userMap.get(packageEntity.getOperatorUserId()));
packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId())); packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId()));
} }
} }
private void fillDeliveryTransient(List<GaoPackageDelivery> deliveryList) {
if (deliveryList == null || deliveryList.isEmpty()) {
return;
}
List<String> idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList();
Map<String, List<GaoPackageDeliveryItem>> itemMap = deliveryService.mapItemListByDeliveryIdList(idList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList);
List<String> userIdList = deliveryList.stream()
.flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> 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<GaoFundRecord> recordList) {
if (recordList == null || recordList.isEmpty()) {
return;
}
List<String> idList = recordList.stream().map(GaoFundRecord::getId).toList();
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList);
List<String> userIdList = recordList.stream()
.flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> 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) { private void fillDerived(GaoCustomerPackageItem item) {
BigDecimal reserved = item.getReservedQuantity() == null ? BigDecimal.ZERO : item.getReservedQuantity(); BigDecimal reserved = item.getReservedQuantity() == null ? BigDecimal.ZERO : item.getReservedQuantity();
BigDecimal delivered = item.getDeliveredQuantity() == null ? BigDecimal.ZERO : item.getDeliveredQuantity(); BigDecimal fulfilled = item.getFulfilledQuantity() == null ? BigDecimal.ZERO : item.getFulfilledQuantity();
BigDecimal total = item.getTotalQuantity() == null ? BigDecimal.ZERO : item.getTotalQuantity(); BigDecimal total = item.getTotalQuantity() == null ? BigDecimal.ZERO : item.getTotalQuantity();
item.setPendingQuantity(reserved); item.setPendingQuantity(reserved);
item.setAvailableQuantity(total.subtract(reserved).subtract(delivered)); item.setAvailableQuantity(total.subtract(reserved).subtract(fulfilled));
item.setRemainingQuantity(total.subtract(delivered)); item.setRemainingQuantity(total.subtract(fulfilled));
} }
} }
@@ -127,6 +127,7 @@ public class GaoEventController {
@PostMapping("/create") @PostMapping("/create")
public void create(@RequestBody GaoEvent event) { public void create(@RequestBody GaoEvent event) {
event.setStoreId(storeScopeService.resolveStoreId(event.getStoreId())); event.setStoreId(storeScopeService.resolveStoreId(event.getStoreId()));
validatePackageConfigBinding(event);
service.create(event); service.create(event);
} }
@@ -141,9 +142,21 @@ public class GaoEventController {
TimiException.required(dbEvent, "not found event"); TimiException.required(dbEvent, "not found event");
storeScopeService.checkStoreId(dbEvent.getStoreId()); storeScopeService.checkStoreId(dbEvent.getStoreId());
event.setStoreId(dbEvent.getStoreId()); event.setStoreId(dbEvent.getStoreId());
validatePackageConfigBinding(event);
service.update(event); service.update(event);
} }
private void validatePackageConfigBinding(GaoEvent event) {
if (event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE) {
TimiException.required(event.getPackageConfigId(), "购买套餐事件必须绑定套餐配置");
GaoPackageConfig packageConfig = packageConfigService.get(event.getPackageConfigId());
TimiException.required(packageConfig, "购买套餐事件绑定的套餐配置不存在");
TimiException.requiredTrue(event.getStoreId().equals(packageConfig.getStoreId()), "购买事件和套餐配置不属于同一门店");
} else {
TimiException.requiredNull(event.getPackageConfigId(), "普通事件不能绑定套餐配置");
}
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE) @RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE)
@@ -6,9 +6,11 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService; import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
@@ -36,6 +38,7 @@ import java.util.stream.Stream;
public class GaoFundRecordController { public class GaoFundRecordController {
private final GaoFundRecordService service; private final GaoFundRecordService service;
private final GaoOrderPaymentService orderPaymentService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final UserService userService; private final UserService userService;
@@ -69,7 +72,14 @@ public class GaoFundRecordController {
@RequireGaoPermission(GaoPermissionCode.FUND_REVOKE) @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE)
@PostMapping("/revoke") @PostMapping("/revoke")
public GaoFundRecord revoke(@RequestBody GaoFundRevokeRequest request) { public GaoFundRecord revoke(@RequestBody GaoFundRevokeRequest request) {
GaoFundRecord result = service.revoke(request); GaoOrderPaymentRevokeRequest orderRequest = new GaoOrderPaymentRevokeRequest();
orderRequest.setRequestNo(request.getRequestNo());
orderRequest.setFundRecordId(request.getFundRecordId());
orderRequest.setOccurredAt(request.getOccurredAt());
orderRequest.setReason(request.getReason());
orderRequest.setAttachmentList(request.getAttachmentList());
orderRequest.setRemark(request.getRemark());
GaoFundRecord result = orderPaymentService.revoke(orderRequest);
fillTransient(List.of(result)); fillTransient(List.of(result));
return result; return result;
} }
@@ -0,0 +1,146 @@
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.GaoOrderFulfillment;
import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
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-09-02
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/order/fulfillment")
public class GaoOrderFulfillmentController {
private final GaoOrderFulfillmentService service;
private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService;
private final UserService userService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_READ)
@PostMapping("/list")
public PageResult<GaoOrderFulfillment> list(@RequestBody GaoOrderFulfillmentPage page) {
TimiException.required(page, "not found fulfillment page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoOrderFulfillment> result = service.pageByQuery(page);
fillTransient(result.getList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_READ)
@PostMapping("/detail")
public GaoOrderFulfillment detail(@RequestParam String id) {
GaoOrderFulfillment result = service.detail(id);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_CREATE)
@PostMapping("/create")
public GaoOrderFulfillment create(@RequestBody GaoOrderFulfillmentCreateRequest request) {
GaoOrderFulfillment result = service.create(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY)
@PostMapping("/prepare-stock")
public GaoOrderFulfillment prepareStock(@RequestBody GaoOrderFulfillmentPrepareStockRequest request) {
GaoOrderFulfillment result = service.prepareStock(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY)
@PostMapping("/verify")
public GaoOrderFulfillment verify(@RequestBody GaoOrderFulfillmentActionRequest request) {
GaoOrderFulfillment result = service.verify(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY)
@PostMapping("/reject")
public GaoOrderFulfillment reject(@RequestBody GaoOrderFulfillmentActionRequest request) {
GaoOrderFulfillment result = service.reject(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_CREATE)
@PostMapping("/cancel")
public GaoOrderFulfillment cancel(@RequestBody GaoOrderFulfillmentActionRequest request) {
GaoOrderFulfillment result = service.cancel(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_REVOKE)
@PostMapping("/revoke")
public GaoOrderFulfillment revoke(@RequestBody GaoOrderFulfillmentRevokeRequest request) {
GaoOrderFulfillment result = service.revoke(request);
fillTransient(List.of(result));
return result;
}
private void fillTransient(List<GaoOrderFulfillment> fulfillmentList) {
if (fulfillmentList == null || fulfillmentList.isEmpty()) {
return;
}
List<String> idList = fulfillmentList.stream().map(GaoOrderFulfillment::getId).toList();
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_ORDER_FULFILLMENT, idList);
List<String> userIdList = fulfillmentList.stream()
.flatMap(item -> Stream.of(item.getOperatorUserId(), item.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
for (GaoOrderFulfillment fulfillment : fulfillmentList) {
fulfillment.setAttachmentList(attachmentMap.getOrDefault(fulfillment.getId(), List.of()));
fulfillment.setOperator(userMap.get(fulfillment.getOperatorUserId()));
fulfillment.setVerifier(userMap.get(fulfillment.getVerifierUserId()));
}
}
}
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
@@ -33,6 +34,7 @@ import java.util.Map;
public class GaoPackageConfigController { public class GaoPackageConfigController {
private final GaoPackageConfigService service; private final GaoPackageConfigService service;
private final GaoEventService eventService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final UserService userService; private final UserService userService;
@@ -116,6 +118,7 @@ public class GaoPackageConfigController {
GaoPackageConfig config = service.get(id); GaoPackageConfig config = service.get(id);
TimiException.required(config, "not found package config"); TimiException.required(config, "not found package config");
storeScopeService.checkStoreId(config.getStoreId()); storeScopeService.checkStoreId(config.getStoreId());
TimiException.requiredTrue(0 == eventService.countByPackageConfigId(id), "套餐配置已绑定购买事件,不能删除");
service.delete(id); service.delete(id);
} }
} }
@@ -1,140 +0,0 @@
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<GaoPackageDelivery> list(@RequestBody GaoPackageDeliveryPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoPackageDelivery> 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<GaoPackageDelivery> deliveryList) {
if (deliveryList == null || deliveryList.isEmpty()) {
return;
}
List<String> idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList();
Map<String, List<GaoPackageDeliveryItem>> itemMap = service.mapItemListByDeliveryIdList(idList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList);
List<String> userIdList = deliveryList.stream()
.flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> 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()));
}
}
}
@@ -0,0 +1,154 @@
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.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.api.modules.gao.service.GaoProductCategoryService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage;
import com.imyeyu.api.modules.gao.vo.GaoProductPage;
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;
/// GAO 产品库接口
///
/// @author Codex
/// @since 2026-09-02
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/product")
public class GaoProductController {
private final GaoProductCategoryService categoryService;
private final GaoProductService productService;
private final GaoStoreScopeService storeScopeService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_READ)
@PostMapping("/category/list")
public PageResult<GaoProductCategory> categoryList(@RequestBody GaoProductCategoryPage page) {
TimiException.required(page, "not found category page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return categoryService.pageByQuery(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_CREATE)
@PostMapping("/category/create")
public String categoryCreate(@RequestBody GaoProductCategory category) {
TimiException.required(category, "not found category");
category.setStoreId(storeScopeService.resolveStoreId(category.getStoreId()));
categoryService.create(category);
return category.getId();
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE)
@PostMapping("/category/update")
public void categoryUpdate(@RequestBody GaoProductCategory request) {
TimiException.required(request, "not found category");
TimiException.required(request.getId(), "not found category.id");
GaoProductCategory category = categoryService.get(request.getId());
TimiException.required(category, "not found category");
storeScopeService.checkStoreId(category.getStoreId());
request.setStoreId(category.getStoreId());
categoryService.update(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE)
@PostMapping("/category/sort")
public void categorySort(@RequestBody List<String> idList) {
categoryService.sort(idList);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE)
@PostMapping("/category/disable")
public void categoryDisable(@RequestParam String id) {
categoryService.disable(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_READ)
@PostMapping("/list")
public PageResult<GaoProduct> list(@RequestBody GaoProductPage page) {
TimiException.required(page, "not found product page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return productService.pageByQuery(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_READ)
@PostMapping("/detail")
public GaoProduct detail(@RequestParam String id) {
GaoProduct result = productService.detail(id);
storeScopeService.checkStoreId(result.getStoreId());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_CREATE)
@PostMapping("/create")
public String create(@RequestBody GaoProduct product) {
TimiException.required(product, "not found product");
product.setStoreId(storeScopeService.resolveStoreId(product.getStoreId()));
productService.create(product);
return product.getId();
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoProduct request) {
TimiException.required(request, "not found product");
TimiException.required(request.getId(), "not found product.id");
GaoProduct product = productService.get(request.getId());
TimiException.required(product, "not found product");
storeScopeService.checkStoreId(product.getStoreId());
request.setStoreId(product.getStoreId());
productService.update(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE)
@PostMapping("/disable")
public void disable(@RequestParam String id) {
GaoProduct product = productService.get(id);
TimiException.required(product, "not found product");
storeScopeService.checkStoreId(product.getStoreId());
productService.disable(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_READ)
@PostMapping("/sellable/list")
public List<GaoProduct> sellableList(@RequestBody GaoProductPage page) {
TimiException.required(page, "not found product page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return productService.listSellable(page);
}
}
@@ -0,0 +1,110 @@
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.GaoProductStock;
import com.imyeyu.api.modules.gao.entity.GaoStockRecord;
import com.imyeyu.api.modules.gao.service.GaoProductStockService;
import com.imyeyu.api.modules.gao.service.GaoStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage;
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;
/// GAO 产品库存接口
///
/// @author Codex
/// @since 2026-09-02
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/product/stock")
public class GaoProductStockController {
private final GaoProductStockService stockService;
private final GaoStockRecordService recordService;
private final GaoStoreScopeService storeScopeService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ)
@PostMapping("/list")
public PageResult<GaoProductStock> list(@RequestBody GaoProductStockPage page) {
TimiException.required(page, "not found stock page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return stockService.pageByQuery(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ)
@PostMapping("/detail")
public GaoProductStock detail(@RequestParam(required = false) String storeId, @RequestParam String productId) {
return stockService.detail(storeScopeService.resolveStoreId(storeId), productId);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
@PostMapping("/initial-in")
public GaoProductStock initialIn(@RequestBody GaoProductStockChangeRequest request) {
return stockService.initialIn(normalize(request));
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
@PostMapping("/manual-in")
public GaoProductStock manualIn(@RequestBody GaoProductStockChangeRequest request) {
return stockService.manualIn(normalize(request));
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
@PostMapping("/manual-out")
public GaoProductStock manualOut(@RequestBody GaoProductStockChangeRequest request) {
return stockService.manualOut(normalize(request));
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
@PostMapping("/stocktake-gain")
public GaoProductStock stocktakeGain(@RequestBody GaoProductStockChangeRequest request) {
return stockService.stocktakeGain(normalize(request));
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
@PostMapping("/stocktake-loss")
public GaoProductStock stocktakeLoss(@RequestBody GaoProductStockChangeRequest request) {
return stockService.stocktakeLoss(normalize(request));
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ)
@PostMapping("/record/list")
public PageResult<GaoStockRecord> recordList(@RequestBody GaoStockRecordPage page) {
TimiException.required(page, "not found stock record page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return recordService.pageByQuery(page);
}
private GaoProductStockChangeRequest normalize(GaoProductStockChangeRequest request) {
TimiException.required(request, "not found stock change request");
request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId()));
return request;
}
}
@@ -0,0 +1,133 @@
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.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
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;
/// GAO 销售订单和资金接口
///
/// @author Codex
/// @since 2026-09-02
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/sales/order")
public class GaoSalesOrderController {
private final GaoSalesOrderService service;
private final GaoOrderPaymentService paymentService;
private final GaoStoreScopeService storeScopeService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ)
@PostMapping("/list")
public PageResult<GaoSalesOrder> list(@RequestBody GaoSalesOrderPage page) {
TimiException.required(page, "not found sales order page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return service.pageByQuery(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ)
@PostMapping("/detail")
public GaoSalesOrder detail(@RequestParam String id) {
return service.detail(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.SALES_ORDER_CREATE)
@PostMapping("/direct-product/create")
public GaoSalesOrder createDirectProduct(@RequestBody GaoDirectProductOrderCreateRequest request) {
TimiException.required(request, "not found direct product order request");
request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId()));
return service.createDirectProductOrder(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.SALES_ORDER_CANCEL)
@PostMapping("/cancel")
public GaoSalesOrder cancel(@RequestBody GaoSalesOrderCancelRequest request) {
return service.cancel(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/payment/receive")
public GaoFundRecord receive(@RequestBody GaoOrderPaymentReceiveRequest request) {
return paymentService.receive(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY)
@PostMapping("/payment/verify")
public GaoFundRecord verify(@RequestParam String id) {
return paymentService.verify(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY)
@PostMapping("/payment/reject")
public GaoFundRecord reject(@RequestBody GaoOrderPaymentActionRequest request) {
return paymentService.reject(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/payment/cancel")
public GaoFundRecord cancelPayment(@RequestBody GaoOrderPaymentActionRequest request) {
return paymentService.cancel(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REFUND)
@PostMapping("/refund")
public GaoFundRecord refund(@RequestBody GaoOrderPaymentRefundRequest request) {
return paymentService.refund(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REFUND)
@PostMapping("/refund/confirm-none")
public GaoFundRecord confirmNoRefund(@RequestBody GaoOrderPaymentNoRefundRequest request) {
return paymentService.confirmNoRefund(request);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REVOKE)
@PostMapping("/fund/revoke")
public GaoFundRecord revoke(@RequestBody GaoOrderPaymentRevokeRequest request) {
return paymentService.revoke(request);
}
}
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.service.GaoStoreService; import com.imyeyu.api.modules.gao.service.GaoStoreService;
@@ -64,6 +65,16 @@ public class GaoStoreController {
return store; return store;
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
@PostMapping("/direct-product-sale-event/list")
public List<GaoEvent> listDirectProductSaleEvents(@RequestParam String storeId) {
GaoStore store = service.get(storeId);
storeScopeService.checkStore(store);
return service.listDirectProductSaleEvents(storeId);
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_CREATE) @RequireGaoPermission(GaoPermissionCode.STORE_CREATE)
@@ -90,6 +101,7 @@ public class GaoStoreController {
dbStore.setAddress(store.getAddress()); dbStore.setAddress(store.getAddress());
dbStore.setRemark(store.getRemark()); dbStore.setRemark(store.getRemark());
dbStore.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false)); dbStore.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
dbStore.setDirectProductSaleEventId(store.getDirectProductSaleEventId());
service.update(dbStore); service.update(dbStore);
} }
} }
@@ -29,80 +29,27 @@ public class GaoCustomerPackage extends UUIDEntity {
CANCELLED CANCELLED
} }
/// 付款状态
public enum PaymentStatus {
/// 未付款
UNPAID,
/// 部分付款
PARTIALLY_PAID,
/// 已付清
PAID
}
/// 退款状态
public enum RefundStatus {
/// 无需退款
NOT_REQUIRED,
/// 待退款或待确认无需退款
PENDING,
/// 已完成退款处理
REFUNDED
}
/// 门店 ID /// 门店 ID
private String storeId; private String storeId;
/// 销售订单 ID
private String salesOrderId;
/// 客户 ID /// 客户 ID
private String customerId; private String customerId;
/// 来源套餐配置 ID /// 来源套餐配置 ID
private String packageConfigId; private String packageConfigId;
/// 购买事件登记 ID
private String purchaseEventRecordId;
/// 购买幂等请求号
private String requestNo;
/// 套餐名称快照 /// 套餐名称快照
private String packageNameSnapshot; private String packageNameSnapshot;
/// 标准价快照,单位分 /// 标准价快照,单位分
private Long standardAmountSnapshot; private Long standardAmountSnapshot;
/// 实际成交价,单位分
private Long saleAmount;
/// 待核销收款合计,单位分
private Long pendingPaidAmount;
/// 累计有效收款,单位分
private Long paidAmount;
/// 累计有效退款,单位分
private Long refundedAmount;
/// 套餐状态 /// 套餐状态
private Status status; private Status status;
/// 付款状态
private PaymentStatus paymentStatus;
/// 退款状态
private RefundStatus refundStatus;
/// 购买时间
private Long purchasedAt;
/// 购买登记人
private String operatorUserId;
/// 取消时间 /// 取消时间
private Long cancelledAt; private Long cancelledAt;
@@ -119,14 +66,6 @@ public class GaoCustomerPackage extends UUIDEntity {
@Transient @Transient
private GaoCustomer customer; private GaoCustomer customer;
/// 购买事件登记
@Transient
private GaoEventRecord purchaseEventRecord;
/// 购买操作人
@Transient
private User operator;
/// 取消操作人 /// 取消操作人
@Transient @Transient
private User cancelOperator; private User cancelOperator;
@@ -135,11 +74,7 @@ public class GaoCustomerPackage extends UUIDEntity {
@Transient @Transient
private List<GaoCustomerPackageItem> itemList; private List<GaoCustomerPackageItem> itemList;
/// 发放历史 /// 统一履约历史
@Transient @Transient
private List<GaoPackageDelivery> deliveryList; private List<GaoOrderFulfillment> fulfillmentList;
/// 资金历史
@Transient
private List<GaoFundRecord> fundRecordList;
} }
@@ -18,20 +18,29 @@ public class GaoCustomerPackageItem extends UUIDEntity {
/// 客户套餐 ID /// 客户套餐 ID
private String customerPackageId; private String customerPackageId;
/// 产品 ID
private String productId;
/// 产品单位 ID
private String productUnitId;
/// 产品名称快照 /// 产品名称快照
private String productNameSnapshot; private String productNameSnapshot;
/// 产品单位快照 /// 产品单位快照
private String unitSnapshot; private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 总权益数量 /// 总权益数量
private BigDecimal totalQuantity; private BigDecimal totalQuantity;
/// 待核销占用数量 /// 待核销占用数量
private BigDecimal reservedQuantity; private BigDecimal reservedQuantity;
/// 已核销净发放数量 /// 已履约数量
private BigDecimal deliveredQuantity; private BigDecimal fulfilledQuantity;
/// 排序值 /// 排序值
private Integer sort; private Integer sort;
@@ -9,7 +9,7 @@ import lombok.EqualsAndHashCode;
import java.util.List; import java.util.List;
/// GAO 通用资金操作流水 /// GAO 销售订单资金操作流水
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-08-31
@@ -20,8 +20,8 @@ public class GaoFundRecord extends UUIDEntity {
/// 资金业务类型 /// 资金业务类型
public enum BizType { public enum BizType {
/// 客户套餐 /// 销售订单
CUSTOMER_PACKAGE SALES_ORDER
} }
/// 资金操作类型 /// 资金操作类型
@@ -9,27 +9,30 @@ import lombok.EqualsAndHashCode;
import java.util.List; import java.util.List;
/// GAO 套餐发放操作 /// GAO 统一履约单
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class GaoPackageDelivery extends UUIDEntity { public class GaoOrderFulfillment extends UUIDEntity {
/// 发放操作类型 /// 履约来源类型
public enum OperationType { public enum FulfillmentType {
/// 正常发放 /// 客户套餐权益
DELIVER, PACKAGE_ENTITLEMENT,
/// 整单冲正 /// 直接产品订单项
REVOKE DIRECT_PRODUCT
} }
/// 发放状态 /// 履约状态
public enum Status { public enum Status {
/// 等待库存
WAITING_STOCK,
/// 待核销 /// 待核销
PENDING, PENDING,
@@ -42,12 +45,12 @@ public class GaoPackageDelivery extends UUIDEntity {
/// 提交人已撤销 /// 提交人已撤销
CANCELLED, CANCELLED,
/// 已核销后被整单冲正 /// 已核销后冲正
REVOKED REVOKED
} }
/// 客户套餐 ID /// 销售订单 ID
private String customerPackageId; private String salesOrderId;
/// 门店 ID /// 门店 ID
private String storeId; private String storeId;
@@ -55,10 +58,10 @@ public class GaoPackageDelivery extends UUIDEntity {
/// 客户 ID /// 客户 ID
private String customerId; private String customerId;
/// 操作类型 /// 履约来源类型
private OperationType operationType; private FulfillmentType fulfillmentType;
/// 发放状态 /// 履约状态
private Status status; private Status status;
/// 客户端幂等请求号 /// 客户端幂等请求号
@@ -67,7 +70,7 @@ public class GaoPackageDelivery extends UUIDEntity {
/// 提交人 /// 提交人
private String operatorUserId; private String operatorUserId;
/// 实际发放时间 /// 实际履约时间
private Long deliveredAt; private Long deliveredAt;
/// 核销或拒绝操作人 /// 核销或拒绝操作人
@@ -76,8 +79,8 @@ public class GaoPackageDelivery extends UUIDEntity {
/// 核销或拒绝时间 /// 核销或拒绝时间
private Long verifiedAt; private Long verifiedAt;
/// 冲正单指向原发放单 /// 冲正单指向原履约单
private String sourceDeliveryId; private String sourceFulfillmentId;
/// 拒绝、撤销或冲正原因 /// 拒绝、撤销或冲正原因
private String reason; private String reason;
@@ -85,7 +88,7 @@ public class GaoPackageDelivery extends UUIDEntity {
/// 备注 /// 备注
private String remark; private String remark;
/// 操作人 /// 提交人
@Transient @Transient
private User operator; private User operator;
@@ -93,13 +96,17 @@ public class GaoPackageDelivery extends UUIDEntity {
@Transient @Transient
private User verifier; private User verifier;
/// 原发放单 /// 原履约单
@Transient @Transient
private GaoPackageDelivery sourceDelivery; private GaoOrderFulfillment sourceFulfillment;
/// 发放明细 /// 履约明细
@Transient @Transient
private List<GaoPackageDeliveryItem> itemList; private List<GaoOrderFulfillmentItem> itemList;
/// 库存处理流水
@Transient
private List<GaoStockRecord> stockRecordList;
/// 附件 /// 附件
@Transient @Transient
@@ -0,0 +1,58 @@
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-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoOrderFulfillmentItem extends UUIDEntity {
/// 履约单 ID
private String fulfillmentId;
/// 产品 ID
private String productId;
/// 产品单位 ID
private String productUnitId;
/// 直接订单明细 ID
private String sourceOrderItemId;
/// 客户套餐权益项 ID
private String sourceCustomerPackageItemId;
/// 本次履约数量
private BigDecimal quantity;
/// 基础单位数量
private BigDecimal baseQuantity;
/// 操作前待履约数量
private BigDecimal reservedBefore;
/// 操作后待履约数量
private BigDecimal reservedAfter;
/// 操作前已履约数量
private BigDecimal fulfilledBefore;
/// 操作后已履约数量
private BigDecimal fulfilledAfter;
/// 产品名称快照
private String productNameSnapshot;
/// 单位快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
}
@@ -17,11 +17,20 @@ public class GaoPackageConfigItem extends UUIDEntity {
/// 套餐配置 ID /// 套餐配置 ID
private String packageConfigId; private String packageConfigId;
/// 产品名称 /// 产品 ID
private String productName; private String productId;
/// 产品单位 /// 产品单位 ID
private String unit; private String productUnitId;
/// 产品名称快照
private String productNameSnapshot;
/// 单位快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 默认数量 /// 默认数量
private BigDecimal defaultQuantity; private BigDecimal defaultQuantity;
@@ -1,43 +0,0 @@
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;
}
@@ -0,0 +1,109 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
/// GAO 产品
///
/// @author Codex
/// @since 2026-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoProduct extends UUIDEntity {
/// 产品类型
public enum ProductType {
/// 服务,不管理库存
SERVICE,
/// 实物,默认管理库存
GOODS,
/// 消耗品,可选择是否管理库存
CONSUMABLE
}
/// 产品附件类型
public enum AttachType {
/// 产品图片
PHOTO,
/// 其他产品文件
OTHER
}
/// 产品状态
public enum Status {
/// 启用
ACTIVE,
/// 停用
INACTIVE
}
/// 门店 ID
private String storeId;
/// 分类 ID
private String categoryId;
/// 门店内产品编码
private String code;
/// 产品名称
private String name;
/// 产品类型
private ProductType productType;
/// 基础库存单位名称
private String baseUnit;
/// true 为启用库存管理
private Boolean stockEnabled;
/// true 为允许业务数量保留小数
private Boolean allowDecimalQuantity;
/// 状态
private Status status;
/// 建议售价,单位分
private Long salePrice;
/// 参考成本,单位分
private Long referenceCost;
/// 最低库存数量,基础单位
private BigDecimal minStockQuantity;
/// 排序值
private Integer sort;
/// 备注
private String remark;
/// 最后修改用户 ID
private String updatedBy;
/// 产品分类
@Transient
private GaoProductCategory category;
/// 产品单位列表
@Transient
private List<GaoProductUnit> unitList;
/// 产品附件列表
@Transient
private List<Attachment> attachmentList;
}
@@ -0,0 +1,36 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// GAO 产品分类
///
/// @author Codex
/// @since 2026-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoProductCategory extends UUIDEntity {
/// 分类状态
public enum Status {
/// 启用
ACTIVE,
/// 停用
INACTIVE
}
/// 门店 ID
private String storeId;
/// 分类名称
private String name;
/// 排序值
private Integer sort;
/// 状态
private Status status;
}
@@ -0,0 +1,40 @@
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-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoProductStock extends UUIDEntity {
/// 门店 ID
private String storeId;
/// 产品 ID
private String productId;
/// 实际库存,基础单位
private BigDecimal onHandQuantity;
/// 已预占库存,基础单位
private BigDecimal reservedQuantity;
/// 最后操作人
private String updatedBy;
/// 产品
@Transient
private GaoProduct product;
/// 可用库存,基础单位
@Transient
private BigDecimal availableQuantity;
}
@@ -0,0 +1,37 @@
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-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoProductUnit extends UUIDEntity {
/// 产品 ID
private String productId;
/// 单位名称
private String name;
/// 折算为基础单位的比例
private BigDecimal conversionToBase;
/// true 为基础库存单位
private Boolean isBase;
/// true 为允许采购入库
private Boolean allowPurchase;
/// true 为允许销售或套餐配置
private Boolean allowSale;
/// 排序值
private Integer sort;
}
@@ -0,0 +1,157 @@
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-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoSalesOrder extends UUIDEntity {
/// 订单类型
public enum OrderType {
/// 套餐订单
PACKAGE,
/// 直接产品订单
DIRECT_PRODUCT
}
/// 订单状态
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;
/// 门店内订单号
private String orderNo;
/// 订单类型
private OrderType orderType;
/// 订单状态
private Status status;
/// 付款状态
private PaymentStatus paymentStatus;
/// 退款状态
private RefundStatus refundStatus;
/// 成交金额,单位分
private Long saleAmount;
/// 待核销收款,单位分
private Long pendingPaidAmount;
/// 有效收款,单位分
private Long paidAmount;
/// 有效退款,单位分
private Long refundedAmount;
/// 来源事件记录 ID
private String sourceEventRecordId;
/// 创建订单幂等号
private String requestNo;
/// 下单时间
private Long purchasedAt;
/// 下单操作人
private String operatorUserId;
/// 取消时间
private Long cancelledAt;
/// 取消操作人
private String cancelOperatorUserId;
/// 取消原因
private String cancelReason;
/// 备注
private String remark;
/// 客户
@Transient
private GaoCustomer customer;
/// 下单操作人
@Transient
private User operator;
/// 取消操作人
@Transient
private User cancelOperator;
/// 直接产品订单明细
@Transient
private List<GaoSalesOrderItem> itemList;
/// 资金流水
@Transient
private List<GaoFundRecord> fundRecordList;
/// 来源事件记录
@Transient
private GaoEventRecord sourceEventRecord;
/// 未收金额,单位分
@Transient
private Long unpaidAmount;
/// 订单附件
@Transient
private List<Attachment> attachmentList;
}
@@ -0,0 +1,55 @@
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-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoSalesOrderItem extends UUIDEntity {
/// 销售订单 ID
private String salesOrderId;
/// 产品 ID
private String productId;
/// 产品单位 ID
private String productUnitId;
/// 产品名称快照
private String productNameSnapshot;
/// 单位快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 购买数量
private BigDecimal quantity;
/// 待履约数量
private BigDecimal reservedQuantity;
/// 已履约数量
private BigDecimal fulfilledQuantity;
/// 实际单价,单位分
private Long unitPrice;
/// 行金额,单位分
private Long lineAmount;
/// 排序值
private Integer sort;
/// 备注
private String remark;
}
@@ -0,0 +1,145 @@
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.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.IDEntity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/// GAO 库存变更流水
///
/// @author Codex
/// @since 2026-09-02
@Data
public class GaoStockRecord implements IDEntity<String>, Creatable {
/// 库存流水业务类型
public enum BizType {
/// 人工库存操作
PRODUCT_STOCK,
/// 订单履约库存操作
ORDER_FULFILLMENT
}
/// 库存操作类型
public enum OperationType {
/// 期初入库
INITIAL_IN,
/// 手工入库
MANUAL_IN,
/// 手工出库
MANUAL_OUT,
/// 履约预占
ORDER_RESERVE,
/// 履约释放预占
ORDER_RELEASE,
/// 履约出库
ORDER_OUT,
/// 履约冲正入库
ORDER_REVOKE,
/// 盘盈
STOCKTAKE_GAIN,
/// 盘亏
STOCKTAKE_LOSS
}
@Id
@AutoUUID
private String id;
/// 门店 ID
private String storeId;
/// 产品 ID
private String productId;
/// 产品单位 ID
private String productUnitId;
/// 产品名称快照
private String productNameSnapshot;
/// 单位名称快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 业务数量
private BigDecimal quantity;
/// 基础单位数量
private BigDecimal baseQuantity;
/// 操作类型
private OperationType operationType;
/// 实际库存增量
private BigDecimal onHandDelta;
/// 预占库存增量
private BigDecimal reservedDelta;
/// 操作前实际库存
private BigDecimal onHandBefore;
/// 操作后实际库存
private BigDecimal onHandAfter;
/// 操作前预占库存
private BigDecimal reservedBefore;
/// 操作后预占库存
private BigDecimal reservedAfter;
/// 来源业务类型
private BizType bizType;
/// 来源业务 ID
private String bizId;
/// 冲正来源流水 ID
private String sourceRecordId;
/// 幂等请求号
private String requestNo;
/// 操作用户 ID
private String operatorUserId;
/// 发生时间
private Long occurredAt;
/// 原因
private String reason;
/// 备注
private String remark;
private Long createdAt;
/// 操作用户
@Transient
private User operator;
/// 附件
@Transient
private List<Attachment> attachmentList;
}
@@ -54,6 +54,9 @@ public class GaoStore extends UUIDEntity {
/// 排序值 /// 排序值
private Integer sort; private Integer sort;
/// 直接产品订单使用的消费事件 ID
private String directProductSaleEventId;
public boolean isActivated() { public boolean isActivated() {
return status == Status.ACTIVE; return status == Status.ACTIVE;
} }
@@ -25,6 +25,6 @@ public interface GaoCustomerPackageItemMapper extends BaseMapper<GaoCustomerPack
/// 调整待核销占用数量 /// 调整待核销占用数量
int updateReservedQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta); int updateReservedQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta);
/// 调整已核销发放数量 /// 调整已履约数量
int updateDeliveredQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta); int updateFulfilledQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta);
} }
@@ -11,17 +11,11 @@ import org.apache.ibatis.annotations.Param;
/// @since 2026-08-31 /// @since 2026-08-31
public interface GaoCustomerPackageMapper extends BaseMapper<GaoCustomerPackage, String> { public interface GaoCustomerPackageMapper extends BaseMapper<GaoCustomerPackage, String> {
/// 按门店和请求号查询购买结果
GaoCustomerPackage selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
/// 按请求号查询购买结果,用于配置删除后的幂等重试
GaoCustomerPackage selectByRequestNoAnyStore(@Param("requestNo") String requestNo);
/// 锁定客户套餐 /// 锁定客户套餐
GaoCustomerPackage selectForUpdate(@Param("id") String id); GaoCustomerPackage selectForUpdate(@Param("id") String id);
/// 按购买事件登记查询客户套餐 /// 按销售订单查询客户套餐
GaoCustomerPackage selectByPurchaseEventRecordId(@Param("purchaseEventRecordId") String purchaseEventRecordId); GaoCustomerPackage selectBySalesOrderId(@Param("salesOrderId") String salesOrderId);
/// 按条件统计客户套餐 /// 按条件统计客户套餐
long countByQuery(GaoCustomerPackagePage page); long countByQuery(GaoCustomerPackagePage page);
@@ -29,12 +23,4 @@ public interface GaoCustomerPackageMapper extends BaseMapper<GaoCustomerPackage,
/// 按条件查询客户套餐 /// 按条件查询客户套餐
java.util.List<GaoCustomerPackage> selectByQuery(GaoCustomerPackagePage page); java.util.List<GaoCustomerPackage> 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);
} }
@@ -29,6 +29,10 @@ public interface GaoEventRecordMapper extends BaseMapper<GaoEventRecord, String>
@Select("SELECT COUNT(*) FROM `gao_event_record` WHERE `event_id` = #{eventId} AND `deleted_at` IS NULL") @Select("SELECT COUNT(*) FROM `gao_event_record` WHERE `event_id` = #{eventId} AND `deleted_at` IS NULL")
long countByEventId(String eventId); long countByEventId(String eventId);
/// 查询事件记录是否被订单引用
@Select("SELECT COUNT(*) FROM `gao_sales_order` WHERE `source_event_record_id` = #{recordId}")
long countBySourceEventRecordId(@Param("recordId") String recordId);
Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt); Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt);
/// 查询事件中已携带数值的登记记录数量 /// 查询事件中已携带数值的登记记录数量
@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillmentItem;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 统一履约明细 Mapper
///
/// @author Codex
/// @since 2026-09-02
public interface GaoOrderFulfillmentItemMapper extends BaseMapper<GaoOrderFulfillmentItem, String> {
/// 按履约单查询明细
List<GaoOrderFulfillmentItem> selectByFulfillmentId(@Param("fulfillmentId") String fulfillmentId);
/// 锁定履约明细
List<GaoOrderFulfillmentItem> selectByFulfillmentIdForUpdate(@Param("fulfillmentId") String fulfillmentId);
}
@@ -0,0 +1,44 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 统一履约 Mapper
///
/// @author Codex
/// @since 2026-09-02
public interface GaoOrderFulfillmentMapper extends BaseMapper<GaoOrderFulfillment, String> {
/// 按门店和幂等号查询履约单
GaoOrderFulfillment selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
/// 按 ID 查询履约单
GaoOrderFulfillment selectById(@Param("id") String id);
/// 锁定履约单
GaoOrderFulfillment selectForUpdate(@Param("id") String id);
/// 按订单查询履约单
List<GaoOrderFulfillment> selectBySalesOrderId(@Param("salesOrderId") String salesOrderId);
/// 按冲正来源查询履约单
GaoOrderFulfillment selectBySourceFulfillmentId(@Param("sourceFulfillmentId") String sourceFulfillmentId);
/// 统计订单中仍占用权益的履约单
long countPendingBySalesOrderId(@Param("salesOrderId") String salesOrderId);
/// 按条件统计履约单
long countByQuery(GaoOrderFulfillmentPage page);
/// 按条件查询履约单
List<GaoOrderFulfillment> selectByQuery(GaoOrderFulfillmentPage page);
/// 更新履约状态
int updateStatus(@Param("id") String id, @Param("status") GaoOrderFulfillment.Status status,
@Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt,
@Param("reason") String reason);
}
@@ -1,21 +0,0 @@
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<GaoPackageDeliveryItem, String> {
/// 按发放单查询明细
List<GaoPackageDeliveryItem> selectByDeliveryId(@Param("deliveryId") String deliveryId);
/// 批量查询发放明细
List<GaoPackageDeliveryItem> selectByDeliveryIdList(@Param("deliveryIdList") Collection<String> deliveryIdList);
}
@@ -1,42 +0,0 @@
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, String> {
/// 按门店和请求号查询发放结果
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<GaoPackageDelivery> selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
/// 按条件统计发放单
long countByQuery(GaoPackageDeliveryPage page);
/// 按条件查询发放单
List<GaoPackageDelivery> 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);
}
@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage;
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-09-02
public interface GaoProductCategoryMapper extends BaseMapper<GaoProductCategory, String> {
/// 查询分类
GaoProductCategory selectById(@Param("id") String id);
/// 按 ID 批量查询分类
List<GaoProductCategory> selectByIdList(@Param("idList") Collection<String> idList);
/// 一次性更新分类排序
void updateSort(@Param("idList") List<String> idList, @Param("updatedAt") long updatedAt);
/// 按条件统计产品分类
long countByQuery(GaoProductCategoryPage page);
/// 按条件查询产品分类
List<GaoProductCategory> selectByQuery(GaoProductCategoryPage page);
/// 查询门店分类
List<GaoProductCategory> selectByStoreId(@Param("storeId") String storeId);
/// 查询门店内同名分类
GaoProductCategory selectByStoreIdAndName(@Param("storeId") String storeId, @Param("name") String name, @Param("excludeId") String excludeId);
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.vo.GaoProductPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 产品 Mapper
///
/// @author Codex
/// @since 2026-09-02
public interface GaoProductMapper extends BaseMapper<GaoProduct, String> {
/// 按条件统计产品
long countByQuery(GaoProductPage page);
/// 按条件查询产品
List<GaoProduct> selectByQuery(GaoProductPage page);
/// 查询产品编码
GaoProduct selectByStoreIdAndCode(@Param("storeId") String storeId, @Param("code") String code, @Param("excludeId") String excludeId);
/// 锁定产品
GaoProduct selectForUpdate(@Param("id") String id);
}
@@ -0,0 +1,39 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 产品库存 Mapper
///
/// @author Codex
/// @since 2026-09-02
public interface GaoProductStockMapper extends BaseMapper<GaoProductStock, String> {
/// 锁定库存余额
GaoProductStock selectForUpdate(@Param("storeId") String storeId, @Param("productId") String productId);
/// 按条件统计库存余额
long countByQuery(GaoProductStockPage page);
/// 按条件查询库存余额
List<GaoProductStock> selectByQuery(GaoProductStockPage page);
/// 按门店和产品查询库存余额
GaoProductStock selectByStoreIdAndProductId(@Param("storeId") String storeId, @Param("productId") String productId);
/// 条件更新库存余额
int updateQuantity(@Param("id") String id, @Param("onHandDelta") java.math.BigDecimal onHandDelta, @Param("reservedDelta") java.math.BigDecimal reservedDelta, @Param("updatedBy") String updatedBy);
/// 条件增加预占库存
int reserve(@Param("id") String id, @Param("quantity") java.math.BigDecimal quantity, @Param("updatedBy") String updatedBy);
/// 条件释放预占库存
int release(@Param("id") String id, @Param("quantity") java.math.BigDecimal quantity, @Param("updatedBy") String updatedBy);
/// 插入库存余额,已存在时忽略
int insertIgnore(GaoProductStock stock);
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
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-09-02
public interface GaoProductUnitMapper extends BaseMapper<GaoProductUnit, String> {
/// 按产品查询有效单位
List<GaoProductUnit> selectByProductId(@Param("productId") String productId);
/// 批量查询产品有效单位
List<GaoProductUnit> selectByProductIdList(@Param("productIdList") Collection<String> productIdList);
/// 锁定单位
GaoProductUnit selectForUpdate(@Param("id") String id);
/// 更新有效单位
int updateUnit(GaoProductUnit unit);
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/// GAO 销售订单明细 Mapper
///
/// @author Codex
/// @since 2026-09-02
public interface GaoSalesOrderItemMapper extends BaseMapper<GaoSalesOrderItem, String> {
/// 查询订单明细
List<GaoSalesOrderItem> selectBySalesOrderId(@Param("salesOrderId") String salesOrderId);
/// 锁定订单明细
GaoSalesOrderItem selectForUpdate(@Param("id") String id);
/// 原子调整待履约数量
int updateReservedQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
/// 原子调整已履约数量
int updateFulfilledQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
}
@@ -0,0 +1,43 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 销售订单 Mapper
///
/// @author Codex
/// @since 2026-09-02
public interface GaoSalesOrderMapper extends BaseMapper<GaoSalesOrder, String> {
/// 按门店和幂等号查询订单
GaoSalesOrder selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
/// 锁定订单
GaoSalesOrder selectForUpdate(@Param("id") String id);
/// 按条件统计订单
long countByQuery(GaoSalesOrderPage page);
/// 按条件查询订单
List<GaoSalesOrder> selectByQuery(GaoSalesOrderPage page);
/// 更新订单付款汇总
int updatePayment(@Param("id") String id, @Param("pendingPaidAmount") Long pendingPaidAmount, @Param("paidAmount") Long paidAmount,
@Param("refundedAmount") Long refundedAmount, @Param("paymentStatus") GaoSalesOrder.PaymentStatus paymentStatus,
@Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus);
/// 带状态条件取消订单
int cancelIf(@Param("id") String id, @Param("fromStatus") GaoSalesOrder.Status fromStatus, @Param("toStatus") GaoSalesOrder.Status toStatus,
@Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId,
@Param("cancelReason") String cancelReason, @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus);
/// 写入订单来源事件
int updateSourceEventRecordId(@Param("id") String id, @Param("sourceEventRecordId") String sourceEventRecordId);
/// 更新订单履约状态
int updateStatus(@Param("id") String id, @Param("status") GaoSalesOrder.Status status);
}
@@ -0,0 +1,30 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoStockRecord;
import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 库存流水 Mapper
///
/// @author Codex
/// @since 2026-09-02
public interface GaoStockRecordMapper extends BaseMapper<GaoStockRecord, String> {
/// 按门店和请求号查询库存流水
List<GaoStockRecord> selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
/// 按业务查询库存流水
List<GaoStockRecord> selectByBiz(@Param("bizType") GaoStockRecord.BizType bizType, @Param("bizId") String bizId);
/// 查询指定业务和产品的库存流水
GaoStockRecord selectByBizProduct(@Param("bizType") GaoStockRecord.BizType bizType, @Param("bizId") String bizId, @Param("productId") String productId, @Param("operationType") GaoStockRecord.OperationType operationType);
/// 按条件统计库存流水
long countByQuery(GaoStockRecordPage page);
/// 按条件查询库存流水
List<GaoStockRecord> selectByQuery(GaoStockRecordPage page);
}
@@ -34,6 +34,9 @@ public interface GaoCustomerPackageService extends BaseService<GaoCustomerPackag
/// @return 已锁定的客户套餐,不存在时为空 /// @return 已锁定的客户套餐,不存在时为空
GaoCustomerPackage getForUpdate(String id); GaoCustomerPackage getForUpdate(String id);
/// 按销售订单查询客户套餐
GaoCustomerPackage getBySalesOrderId(String salesOrderId);
/// 查询客户套餐产品权益 /// 查询客户套餐产品权益
/// ///
/// @param customerPackageId 客户套餐 ID /// @param customerPackageId 客户套餐 ID
@@ -59,26 +62,12 @@ public interface GaoCustomerPackageService extends BaseService<GaoCustomerPackag
/// @return 受影响行数 /// @return 受影响行数
int updateItemReservedQuantity(String itemId, BigDecimal delta); int updateItemReservedQuantity(String itemId, BigDecimal delta);
/// 原子调整已核销发放数量 /// 原子调整已履约数量
/// ///
/// @param itemId 客户套餐产品权益 ID /// @param itemId 客户套餐产品权益 ID
/// @param delta 调整量 /// @param delta 调整量
/// @return 受影响行数 /// @return 受影响行数
int updateItemDeliveredQuantity(String itemId, BigDecimal delta); int updateItemFulfilledQuantity(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);
/// 保存客户套餐聚合状态 /// 保存客户套餐聚合状态
/// ///
@@ -2,10 +2,15 @@ package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordNumberValueDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
@@ -25,6 +30,58 @@ public interface GaoEventRecordService extends BaseService<GaoEventRecord, Strin
/// @return 登记事件记录列表 /// @return 登记事件记录列表
List<GaoEventRecord> listByCustomerId(String customerId); List<GaoEventRecord> listByCustomerId(String customerId);
/// 查询指定事件的登记数量
///
/// @param eventId 事件 ID
/// @return 登记数量
long countByEventId(String eventId);
/// 查询指定事件中已携带数值的登记数量
///
/// @param eventId 事件 ID
/// @return 登记数量
long countByEventIdAndNumberValue(String eventId);
/// 按门店和登记日期查询登记数量
///
/// @param storeId 门店 ID
/// @param registeredDateValue 登记日期值
/// @return 登记数量
long countByStoreIdAndRegisteredDateValue(String storeId, Integer registeredDateValue);
/// 同步登记记录营业日序号快照
///
/// @param storeId 门店 ID
/// @param registeredDateValue 登记日期值
/// @param businessDayNo 营业日序号
void updateBusinessDayNo(String storeId, Integer registeredDateValue, long businessDayNo);
/// 按条件统计登记记录数量
///
/// @param storeId 门店 ID
/// @param customerId 客户 ID
/// @param eventId 事件 ID
/// @param beginAt 起始时间,包含
/// @param endAt 结束时间,不包含
/// @return 登记数量
long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt);
/// 汇总客户指定事件登记数值
///
/// @param storeId 门店 ID
/// @param customerId 客户 ID
/// @param eventId 事件 ID
/// @param beginAt 起始时间,包含
/// @param endAt 结束时间,不包含
/// @return 数值总和
long sumNumberValue(String storeId, String customerId, String eventId, Long beginAt, Long endAt);
/// 回写登记记录获得的积分快照
///
/// @param id 登记记录 ID
/// @param earnedPointValue 获得积分
void updateEarnedPointValue(String id, long earnedPointValue);
/// 为登记记录附加已选择的选项组 /// 为登记记录附加已选择的选项组
/// ///
/// @param recordList 登记记录列表 /// @param recordList 登记记录列表
@@ -53,6 +110,22 @@ public interface GaoEventRecordService extends BaseService<GaoEventRecord, Strin
/// @return 创建后的事件登记 /// @return 创建后的事件登记
GaoEventRecord createPackagePurchaseRecord(GaoEventRecord record); GaoEventRecord createPackagePurchaseRecord(GaoEventRecord record);
/// 由直接产品订单内部创建消费事件记录
///
/// @param record 订单消费事件记录
/// @return 创建后的事件记录
GaoEventRecord createOrderConsumeRecord(GaoEventRecord record);
/// 撤销订单消费事件产生的全部积分
///
/// @param record 订单消费事件记录
void revokeOrderConsumeRecord(GaoEventRecord record);
/// 拒绝修改或删除已被订单引用的事件记录
///
/// @param recordId 事件记录 ID
void rejectMutationIfLinkedOrder(String recordId);
void createWithCustomer(GaoCustomer customer); void createWithCustomer(GaoCustomer customer);
/// 按任意时间范围查询登记事件记录 /// 按任意时间范围查询登记事件记录
@@ -61,6 +134,36 @@ public interface GaoEventRecordService extends BaseService<GaoEventRecord, Strin
/// @return 登记事件记录列表 /// @return 登记事件记录列表
PageResult<GaoEventRecord> pageByRange(GaoEventRecordPage req); PageResult<GaoEventRecord> pageByRange(GaoEventRecordPage req);
/// 按时间范围统计门店登记数
///
/// @param req 门店报表查询请求
/// @return 每日登记数
List<GaoEventRecordDailyStatView> stateDailyByRange(GaoStoreChartReq req);
/// 按时间范围统计门店登记数值
///
/// @param req 门店报表查询请求
/// @return 每日登记数值
List<GaoEventRecordNumberValueDailyStatView> stateNumberValueDailyByRange(GaoStoreChartReq req);
/// 按时间范围查询门店登记日历
///
/// @param req 门店报表查询请求
/// @return 登记日历
List<GaoEventRecordCalendarView> calendarByStoreRange(GaoStoreChartReq req);
/// 按客户和时间范围统计登记数
///
/// @param req 客户报表查询请求
/// @return 每日登记数
List<GaoEventRecordDailyStatView> stateCustomerDailyByRange(GaoCustomerChartReq req);
/// 按客户和时间范围查询登记日历
///
/// @param req 客户报表查询请求
/// @return 登记日历
List<GaoEventRecordCalendarView> calendarByCustomerRange(GaoCustomerChartReq req);
/// 按登记记录列表条件汇总数值 /// 按登记记录列表条件汇总数值
/// ///
/// @param req 登记记录范围查询请求 /// @param req 登记记录范围查询请求
@@ -25,6 +25,12 @@ public interface GaoEventService extends BaseService<GaoEvent, String> {
Map<String, GaoEvent> mapByIdList(Collection<String> idList); Map<String, GaoEvent> mapByIdList(Collection<String> idList);
/// 查询绑定指定套餐配置的事件数量
///
/// @param packageConfigId 套餐配置 ID
/// @return 事件数量
long countByPackageConfigId(String packageConfigId);
Map<String, List<String>> mapRoleCodeListByEventIdList(Collection<String> eventIdList); Map<String, List<String>> mapRoleCodeListByEventIdList(Collection<String> eventIdList);
/// 查询启用的登记事件 /// 查询启用的登记事件
@@ -1,18 +1,13 @@
package com.imyeyu.api.modules.gao.service; package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
import java.util.List; import java.util.List;
/// GAO 通用资金流水服务 /// GAO 销售订单资金流水服务
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-08-31
@@ -21,33 +16,44 @@ public interface GaoFundRecordService extends BaseService<GaoFundRecord, String>
/// 按条件查询资金流水 /// 按条件查询资金流水
PageResult<GaoFundRecord> pageByQuery(GaoFundRecordPage page); PageResult<GaoFundRecord> pageByQuery(GaoFundRecordPage page);
/// 查询客户套餐资金历史 /// 按销售订单查询资金流水
List<GaoFundRecord> listByCustomerPackageId(String customerPackageId); List<GaoFundRecord> listByBizId(String salesOrderId);
/// 登记线下收款 /// 由销售订单资金服务创建资金流水
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 /// @param record 资金记录
/// @return 待核销收款数量 void createByOrderPayment(GaoFundRecord record);
long countPendingReceiveByBizId(String bizId);
/// 查询销售订单待核销收款数量
long countPendingReceiveByBizId(String salesOrderId);
/// 按门店和请求号查询资金记录
///
/// @param storeId 门店 ID
/// @param requestNo 请求号
/// @return 资金记录
GaoFundRecord getByRequestNo(String storeId, String requestNo);
/// 锁定资金记录
///
/// @param id 资金记录 ID
/// @return 资金记录
GaoFundRecord getForUpdate(String id);
/// 查询资金冲正记录
///
/// @param sourceRecordId 原资金记录 ID
/// @return 冲正记录
GaoFundRecord getRevokeBySourceRecordId(String sourceRecordId);
/// 按状态条件更新资金记录
///
/// @param id 资金记录 ID
/// @param fromStatus 原状态
/// @param toStatus 目标状态
/// @param verifierUserId 核销用户 ID
/// @param verifiedAt 核销时间
/// @param reason 原因
/// @return 更新行数
int updateStatusIf(String id, GaoFundRecord.Status fromStatus, GaoFundRecord.Status toStatus, String verifierUserId, Long verifiedAt, String reason);
} }
@@ -0,0 +1,49 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// GAO 统一履约服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoOrderFulfillmentService extends BaseService<GaoOrderFulfillment, String> {
/// 按条件分页查询履约单
PageResult<GaoOrderFulfillment> pageByQuery(GaoOrderFulfillmentPage page);
/// 查询履约单详情
GaoOrderFulfillment detail(String id);
/// 按销售订单查询履约历史
List<GaoOrderFulfillment> listBySalesOrderId(String salesOrderId);
/// 查询销售订单仍占用权益的履约单数量
long countPendingBySalesOrderId(String salesOrderId);
/// 创建履约单
GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request);
/// 为库存不足履约单准备库存
GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request);
/// 核销履约单
GaoOrderFulfillment verify(GaoOrderFulfillmentActionRequest request);
/// 拒绝履约单
GaoOrderFulfillment reject(GaoOrderFulfillmentActionRequest request);
/// 撤销待核销履约单
GaoOrderFulfillment cancel(GaoOrderFulfillmentActionRequest request);
/// 冲正已核销履约单
GaoOrderFulfillment revoke(GaoOrderFulfillmentRevokeRequest request);
}
@@ -0,0 +1,36 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
/// GAO 销售订单资金服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoOrderPaymentService {
/// 登记收款
GaoFundRecord receive(GaoOrderPaymentReceiveRequest request);
/// 核销收款
GaoFundRecord verify(String fundRecordId);
/// 拒绝收款
GaoFundRecord reject(GaoOrderPaymentActionRequest request);
/// 撤销自己提交的收款
GaoFundRecord cancel(GaoOrderPaymentActionRequest request);
/// 登记退款
GaoFundRecord refund(GaoOrderPaymentRefundRequest request);
/// 确认无需退款
GaoFundRecord confirmNoRefund(GaoOrderPaymentNoRefundRequest request);
/// 冲正资金流水
GaoFundRecord revoke(GaoOrderPaymentRevokeRequest request);
}
@@ -1,60 +0,0 @@
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<GaoPackageDelivery, String> {
/// 按条件查询发放单
PageResult<GaoPackageDelivery> pageByQuery(GaoPackageDeliveryPage page);
/// 查询客户套餐发放历史
List<GaoPackageDelivery> listByCustomerPackageId(String customerPackageId);
/// 批量查询发放明细
///
/// @param deliveryIdList 发放单 ID 列表
/// @return 按发放单 ID 分组的发放明细
Map<String, List<GaoPackageDeliveryItem>> mapItemListByDeliveryIdList(Collection<String> 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);
}
@@ -9,6 +9,8 @@ import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// GAO 客户积分流水服务 /// GAO 客户积分流水服务
/// ///
/// @author Codex /// @author Codex
@@ -38,6 +40,18 @@ public interface GaoPointLedgerService extends BaseService<GaoPointLedger, Strin
/// @return 调整流水 /// @return 调整流水
GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId); GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId);
/// 查询登记记录产生的原始规则积分流水
///
/// @param eventRecordId 登记记录 ID
/// @return 原始积分流水
List<GaoPointLedger> listRuleChangeByEventRecordId(String eventRecordId);
/// 按幂等键批量查询积分流水
///
/// @param idempotencyKeyList 幂等键列表
/// @return 已存在流水
List<GaoPointLedger> listByIdempotencyKeyList(List<String> idempotencyKeyList);
/// 分页查询积分流水 /// 分页查询积分流水
/// ///
/// @param page 查询条件 /// @param page 查询条件
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// GAO 产品分类服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoProductCategoryService extends BaseService<GaoProductCategory, String> {
/// 按条件分页查询分类
PageResult<GaoProductCategory> pageByQuery(GaoProductCategoryPage page);
/// 查询门店分类
List<GaoProductCategory> listByStore(String storeId);
/// 批量更新分类排序
void sort(List<String> idList);
/// 停用分类
void disable(String id);
}
@@ -0,0 +1,44 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.gao.vo.GaoProductPage;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// GAO 产品服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoProductService extends BaseService<GaoProduct, String> {
/// 按条件分页查询产品
PageResult<GaoProduct> pageByQuery(GaoProductPage page);
/// 查询产品详情单位
GaoProduct detail(String id);
/// 查询启用且允许销售的产品单位
List<GaoProduct> listSellable(GaoProductPage page);
/// 查询并校验可销售产品单位
GaoProductUnitSelection getSellableProductUnit(String storeId, String productId, String productUnitId);
/// 查询当前门店的有效产品单位
///
/// @param storeId 门店 ID
/// @param productId 产品 ID
/// @param productUnitId 产品单位 ID
/// @param purchase true 为按采购入库用途校验
/// @return 已校验的产品单位
GaoProductUnitSelection getProductUnit(String storeId, String productId, String productUnitId, boolean purchase);
/// 查询产品单位
List<GaoProductUnit> listUnitByProductId(String productId);
/// 停用产品
void disable(String id);
}
@@ -0,0 +1,53 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.entity.GaoStockRecord;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
import com.imyeyu.spring.bean.PageResult;
import java.util.List;
/// GAO 产品库存服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoProductStockService {
/// 查询库存余额
PageResult<GaoProductStock> pageByQuery(GaoProductStockPage page);
/// 查询库存余额详情
GaoProductStock detail(String storeId, String productId);
/// 期初入库
GaoProductStock initialIn(GaoProductStockChangeRequest request);
/// 手工入库
GaoProductStock manualIn(GaoProductStockChangeRequest request);
/// 手工出库
GaoProductStock manualOut(GaoProductStockChangeRequest request);
/// 盘盈
GaoProductStock stocktakeGain(GaoProductStockChangeRequest request);
/// 盘亏
GaoProductStock stocktakeLoss(GaoProductStockChangeRequest request);
/// 为履约预占库存
boolean hasSufficientForFulfillment(String storeId, List<GaoStockChangeItem> items);
/// 为履约预占库存
List<GaoProductStock> reserveForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId);
/// 释放履约预占库存
List<GaoProductStock> releaseForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId);
/// 履约出库
List<GaoProductStock> outForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId);
/// 履约库存冲正入库
List<GaoProductStock> revokeForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId);
}
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import java.math.BigDecimal;
import java.util.List;
/// GAO 产品单位服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoProductUnitService {
/// 校验并规范化产品单位
///
/// @param product 产品
/// @param unitList 单位列表
/// @return 规范化后的单位列表
List<GaoProductUnit> validateAndNormalize(GaoProduct product, List<GaoProductUnit> unitList);
/// 按基础单位换算数量
///
/// @param unit 单位
/// @param quantity 业务数量
/// @return 基础单位数量
BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity);
}
@@ -0,0 +1,72 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.math.BigDecimal;
import java.util.List;
/// GAO 销售订单服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoSalesOrderService extends BaseService<GaoSalesOrder, String> {
/// 按条件分页查询订单
PageResult<GaoSalesOrder> pageByQuery(GaoSalesOrderPage page);
/// 查询订单详情
GaoSalesOrder detail(String id);
/// 创建直接产品订单
GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request);
/// 按门店和幂等号查询订单
///
/// @param storeId 门店 ID
/// @param requestNo 幂等号
/// @return 销售订单,不存在时为空
GaoSalesOrder getByStoreIdAndRequestNo(String storeId, String requestNo);
/// 由套餐购买流程创建套餐订单
GaoSalesOrder createPackageOrder(GaoPackageOrderCreateData data);
/// 锁定订单
GaoSalesOrder getForUpdate(String id);
/// 取消订单
GaoSalesOrder cancel(GaoSalesOrderCancelRequest request);
/// 更新订单付款汇总
void updatePayment(GaoSalesOrder order);
/// 重算付款状态
void recomputePaymentStatus(GaoSalesOrder order);
/// 检查履约权限
void checkFulfillmentPermission(GaoSalesOrder order);
/// 原子调整订单项待履约数量
int updateItemReservedQuantity(String itemId, BigDecimal delta);
/// 原子调整订单项已履约数量
int updateItemFulfilledQuantity(String itemId, BigDecimal delta);
/// 锁定订单明细
GaoSalesOrderItem getItemForUpdate(String itemId);
/// 根据订单明细刷新订单履约状态
void refreshFulfillmentStatus(GaoSalesOrder order);
/// 写入套餐订单履约状态
void updateFulfillmentStatus(GaoSalesOrder order, boolean fulfilled);
/// 查询订单明细
List<GaoSalesOrderItem> listItemByOrderId(String orderId);
}
@@ -0,0 +1,32 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.gao.entity.GaoStockRecord;
import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// GAO 库存流水服务
///
/// @author Codex
/// @since 2026-09-02
public interface GaoStockRecordService extends BaseService<GaoStockRecord, String> {
/// 按条件查询库存流水
PageResult<GaoStockRecord> pageByQuery(GaoStockRecordPage page);
/// 按来源业务查询库存流水
List<GaoStockRecord> listByBiz(GaoStockRecord.BizType bizType, String bizId);
/// 按门店和请求号查询库存流水
List<GaoStockRecord> listByRequestNo(String storeId, String requestNo);
/// 查询指定业务和产品的库存流水
GaoStockRecord getByBizProduct(GaoStockRecord.BizType bizType, String bizId, String productId,
GaoStockRecord.OperationType operationType);
/// 保存库存操作流水及附件
void createOperationRecord(GaoStockRecord record, List<Attachment> attachmentList);
}
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.service; package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
@@ -14,6 +15,18 @@ import java.util.List;
/// @since 2026-08-06 /// @since 2026-08-06
public interface GaoStoreService extends BaseService<GaoStore, String> { public interface GaoStoreService extends BaseService<GaoStore, String> {
/// 校验直接产品订单使用的消费事件
///
/// @param store 门店
/// @return 合法的消费事件
GaoEvent validateDirectProductSaleEvent(GaoStore store);
/// 查询可绑定到直接产品订单的消费事件
///
/// @param storeId 门店 ID
/// @return 当前门店可绑定的消费事件
List<GaoEvent> listDirectProductSaleEvents(String storeId);
/// 查询用户所属门店 /// 查询用户所属门店
/// ///
/// @param userId 用户 ID /// @param userId 用户 ID
@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService; import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
@@ -19,18 +19,18 @@ import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoCustomerChartServiceImplement implements GaoCustomerChartService { public class GaoCustomerChartServiceImplement implements GaoCustomerChartService {
private final GaoEventRecordMapper eventRecordMapper; private final GaoEventRecordService eventRecordService;
@Override @Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoCustomerChartReq req) { public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoCustomerChartReq req) {
checkRangeReq(req); checkRangeReq(req);
return eventRecordMapper.stateCustomerDailyByRange(req); return eventRecordService.stateCustomerDailyByRange(req);
} }
@Override @Override
public List<GaoEventRecordCalendarView> eventRecordCalendar(GaoCustomerChartReq req) { public List<GaoEventRecordCalendarView> eventRecordCalendar(GaoCustomerChartReq req) {
checkRangeReq(req); checkRangeReq(req);
return eventRecordMapper.selectCustomerCalendarByRange(req); return eventRecordService.calendarByCustomerRange(req);
} }
private void checkRangeReq(GaoCustomerChartReq req) { private void checkRangeReq(GaoCustomerChartReq req) {
@@ -9,20 +9,23 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageItemMapper; import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageItemMapper;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper; import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper;
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoEventService; 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.GaoPackageConfigService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem;
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.PermissionChecker; import com.imyeyu.api.modules.user.service.PermissionChecker;
@@ -34,7 +37,6 @@ import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -59,12 +61,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
private final GaoCustomerPackageMapper mapper; private final GaoCustomerPackageMapper mapper;
private final GaoCustomerPackageItemMapper itemMapper; private final GaoCustomerPackageItemMapper itemMapper;
private final ObjectProvider<GaoPackageDeliveryService> deliveryServiceProvider;
private final ObjectProvider<GaoFundRecordService> fundRecordServiceProvider;
private final GaoPackageConfigService packageConfigService; private final GaoPackageConfigService packageConfigService;
private final GaoProductService productService;
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final GaoEventService eventService; private final GaoEventService eventService;
private final GaoEventRecordService eventRecordService; private final GaoEventRecordService eventRecordService;
private final GaoSalesOrderService salesOrderService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final PermissionChecker permissionChecker; private final PermissionChecker permissionChecker;
@@ -80,6 +82,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
return mapper.selectForUpdate(id); return mapper.selectForUpdate(id);
} }
@Override
public GaoCustomerPackage getBySalesOrderId(String salesOrderId) {
TimiException.required(salesOrderId, "not found salesOrderId");
return mapper.selectBySalesOrderId(salesOrderId);
}
@Override @Override
public List<GaoCustomerPackageItem> listItemByCustomerPackageId(String customerPackageId) { public List<GaoCustomerPackageItem> listItemByCustomerPackageId(String customerPackageId) {
return itemMapper.selectByCustomerPackageId(customerPackageId); return itemMapper.selectByCustomerPackageId(customerPackageId);
@@ -96,18 +104,8 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
} }
@Override @Override
public int updateItemDeliveredQuantity(String itemId, BigDecimal delta) { public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateDeliveredQuantity(itemId, delta); return itemMapper.updateFulfilledQuantity(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 @Override
@@ -190,11 +188,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort)); itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort));
request.setItemList(itemList); request.setItemList(itemList);
GaoCustomerPackage existing = mapper.selectByRequestNo(config.getStoreId(), request.getRequestNo()); GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo());
if (existing != null) { if (existingOrder != null) {
verifyPurchaseIdempotency(existing, request); GaoCustomerPackage existingPackage = getBySalesOrderId(existingOrder.getId());
return existing; verifyPurchaseIdempotency(existingOrder, existingPackage, request);
return existingPackage;
} }
verifyConfigSellableItems(config.getStoreId(), configItemList);
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now()); Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
Long now = Time.now(); Long now = Time.now();
@@ -219,34 +219,41 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
eventRecord.setNumberValue(request.getSaleAmount()); eventRecord.setNumberValue(request.getSaleAmount());
} }
GaoEventRecord createdEventRecord = eventRecordService.createPackagePurchaseRecord(eventRecordWithAttachments(eventRecord, request)); GaoEventRecord createdEventRecord = eventRecordService.createPackagePurchaseRecord(eventRecordWithAttachments(eventRecord, request));
GaoPackageOrderCreateData orderData = new GaoPackageOrderCreateData();
orderData.setStoreId(config.getStoreId());
orderData.setCustomerId(customer.getId());
orderData.setPackageConfigId(config.getId());
orderData.setPackageNameSnapshot(config.getName());
orderData.setStandardAmountSnapshot(config.getStandardAmount());
orderData.setSaleAmount(request.getSaleAmount());
orderData.setSourceEventRecordId(createdEventRecord.getId());
orderData.setRequestNo(request.getRequestNo());
orderData.setPurchasedAt(purchasedAt);
orderData.setOperatorUserId(userLoginService.getRequireLoginUserId());
orderData.setRemark(request.getRemark());
GaoSalesOrder salesOrder = salesOrderService.createPackageOrder(orderData);
GaoCustomerPackage result = new GaoCustomerPackage(); GaoCustomerPackage result = new GaoCustomerPackage();
result.setStoreId(config.getStoreId()); result.setStoreId(config.getStoreId());
result.setSalesOrderId(salesOrder.getId());
result.setCustomerId(customer.getId()); result.setCustomerId(customer.getId());
result.setPackageConfigId(config.getId()); result.setPackageConfigId(config.getId());
result.setPurchaseEventRecordId(createdEventRecord.getId());
result.setRequestNo(request.getRequestNo());
result.setPackageNameSnapshot(config.getName()); result.setPackageNameSnapshot(config.getName());
result.setStandardAmountSnapshot(config.getStandardAmount()); result.setStandardAmountSnapshot(config.getStandardAmount());
result.setSaleAmount(request.getSaleAmount());
result.setPendingPaidAmount(0L);
result.setPaidAmount(0L);
result.setRefundedAmount(0L);
result.setStatus(GaoCustomerPackage.Status.ACTIVE); 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()); result.setRemark(request.getRemark());
super.create(result); super.create(result);
for (GaoCustomerPackagePurchaseItem source : itemList) { for (GaoCustomerPackagePurchaseItem source : itemList) {
GaoCustomerPackageItem item = new GaoCustomerPackageItem(); GaoCustomerPackageItem item = new GaoCustomerPackageItem();
item.setCustomerPackageId(result.getId()); item.setCustomerPackageId(result.getId());
item.setProductNameSnapshot(source.getProductName().trim()); item.setProductId(source.getProductId());
item.setUnitSnapshot(source.getUnit().trim()); item.setProductUnitId(source.getProductUnitId());
item.setProductNameSnapshot(source.getProductNameSnapshot());
item.setUnitSnapshot(source.getUnitSnapshot());
item.setConversionToBaseSnapshot(source.getConversionToBaseSnapshot());
item.setTotalQuantity(source.getTotalQuantity()); item.setTotalQuantity(source.getTotalQuantity());
item.setReservedQuantity(ZERO); item.setReservedQuantity(ZERO);
item.setDeliveredQuantity(ZERO); item.setFulfilledQuantity(ZERO);
item.setSort(source.getSort()); item.setSort(source.getSort());
item.setRemark(source.getRemark()); item.setRemark(source.getRemark());
itemMapper.insert(item); itemMapper.insert(item);
@@ -268,18 +275,15 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
TimiException.required(packageEntity, "not found customer package"); TimiException.required(packageEntity, "not found customer package");
storeScopeService.checkStoreId(packageEntity.getStoreId()); storeScopeService.checkStoreId(packageEntity.getStoreId());
TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可取消"); TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可取消");
TimiException.requiredTrue(0 == deliveryServiceProvider.getObject().countPendingByCustomerPackageId(packageEntity.getId()), "存在待核销发放,不能取消套餐"); TimiException.required(packageEntity.getSalesOrderId(), "套餐未关联销售订单");
TimiException.requiredTrue(0 == fundRecordServiceProvider.getObject().countPendingReceiveByBizId(packageEntity.getId()), "存在待核销收款,不能取消套餐"); GaoSalesOrderCancelRequest orderCancelRequest = new GaoSalesOrderCancelRequest();
TimiException.requiredTrue(0 == pendingAmount(packageEntity), "存在待核销收款,不能取消套餐"); orderCancelRequest.setSalesOrderId(packageEntity.getSalesOrderId());
List<GaoCustomerPackageItem> itemList = itemMapper.selectByCustomerPackageId(packageEntity.getId()); orderCancelRequest.setReason(reason);
for (GaoCustomerPackageItem item : itemList) { GaoSalesOrder order = salesOrderService.cancel(orderCancelRequest);
TimiException.requiredTrue(item.getDeliveredQuantity().compareTo(item.getTotalQuantity()) < 0, "已全部履约的套餐不能取消");
}
packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED); packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED);
packageEntity.setCancelledAt(Time.now()); packageEntity.setCancelledAt(order.getCancelledAt());
packageEntity.setCancelOperatorUserId(userLoginService.getRequireLoginUserId()); packageEntity.setCancelOperatorUserId(order.getCancelOperatorUserId());
packageEntity.setCancelReason(reason); packageEntity.setCancelReason(reason);
packageEntity.setRefundStatus(0 < safeLong(packageEntity.getPaidAmount()) ? GaoCustomerPackage.RefundStatus.PENDING : GaoCustomerPackage.RefundStatus.NOT_REQUIRED);
mapper.update(packageEntity); mapper.update(packageEntity);
return packageEntity; return packageEntity;
} }
@@ -307,8 +311,11 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
private List<GaoCustomerPackagePurchaseItem> defaultPurchaseItems(List<GaoPackageConfigItem> configItemList) { private List<GaoCustomerPackagePurchaseItem> defaultPurchaseItems(List<GaoPackageConfigItem> configItemList) {
return configItemList.stream().map(configItem -> { return configItemList.stream().map(configItem -> {
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem(); GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
item.setProductName(configItem.getProductName()); item.setProductId(configItem.getProductId());
item.setUnit(configItem.getUnit()); item.setProductUnitId(configItem.getProductUnitId());
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
item.setUnitSnapshot(configItem.getUnitSnapshot());
item.setConversionToBaseSnapshot(configItem.getConversionToBaseSnapshot());
item.setTotalQuantity(configItem.getDefaultQuantity()); item.setTotalQuantity(configItem.getDefaultQuantity());
item.setSort(configItem.getSort()); item.setSort(configItem.getSort());
item.setRemark(configItem.getRemark()); item.setRemark(configItem.getRemark());
@@ -322,53 +329,65 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
for (int i = 0; i < itemList.size(); i++) { for (int i = 0; i < itemList.size(); i++) {
GaoCustomerPackagePurchaseItem item = itemList.get(i); GaoCustomerPackagePurchaseItem item = itemList.get(i);
TimiException.required(item, "购买产品明细不能为空"); TimiException.required(item, "购买产品明细不能为空");
TimiException.required(item.getProductName(), "购买产品名称不能为空"); TimiException.required(item.getProductId(), "购买产品不能为空");
TimiException.required(item.getUnit(), "购买产品单位不能为空"); TimiException.required(item.getProductUnitId(), "购买产品单位不能为空");
TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空"); TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空");
TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0"); TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0");
TimiException.requiredTrue(3 >= item.getTotalQuantity().scale(), "购买产品数量最多保留三位小数"); TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
String key = item.getProductName().trim() + "|" + item.getUnit().trim(); String key = item.getProductId() + "|" + item.getProductUnitId();
TimiException.requiredTrue(keySet.add(key), "购买套餐内产品名称和单位不能重复"); TimiException.requiredTrue(keySet.add(key), "购买套餐内产品和单位不能重复");
item.setProductName(item.getProductName().trim());
item.setUnit(item.getUnit().trim());
item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
} }
} }
private void verifyConfigSellableItems(String storeId, List<GaoPackageConfigItem> itemList) {
for (GaoPackageConfigItem item : itemList) {
TimiException.required(item.getProductId(), "套餐配置缺少产品");
TimiException.required(item.getProductUnitId(), "套餐配置缺少产品单位");
// 通过产品库唯一选品入口重新校验产品和单位仍处于可销售状态
productService.getSellableProductUnit(storeId, item.getProductId(), item.getProductUnitId());
}
}
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) { private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致"); TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致");
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity())); Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) { for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit()); GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId());
TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致"); TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致");
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
item.setUnitSnapshot(configItem.getUnitSnapshot());
item.setConversionToBaseSnapshot(configItem.getConversionToBaseSnapshot());
TimiException.requiredTrue(item.getSort().equals(configItem.getSort()) TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
&& sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改"); && sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改");
} }
} }
private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) { private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity())); Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) { for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit()); GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId());
TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额"); TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额");
} }
} }
private void verifyPurchaseIdempotency(GaoCustomerPackage existing, GaoCustomerPackagePurchaseRequest request) { private void verifyPurchaseIdempotency(GaoSalesOrder existingOrder, GaoCustomerPackage existingPackage, GaoCustomerPackagePurchaseRequest request) {
TimiException.requiredTrue(existing.getCustomerId().equals(request.getCustomerId()) TimiException.required(existingPackage, "幂等订单未找到客户套餐权益");
&& existing.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用"); TimiException.requiredTrue(existingOrder.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
GaoEventRecord eventRecord = eventRecordService.get(existing.getPurchaseEventRecordId()); && existingOrder.getCustomerId().equals(request.getCustomerId())
&& existingOrder.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用");
GaoEventRecord eventRecord = eventRecordService.get(existingOrder.getSourceEventRecordId());
TimiException.required(eventRecord, "购买事件登记不存在"); TimiException.required(eventRecord, "购买事件登记不存在");
TimiException.requiredTrue(eventRecord.getEventId().equals(request.getEventId()) TimiException.requiredTrue(eventRecord.getEventId().equals(request.getEventId())
&& (request.getPurchasedAt() == null || eventRecord.getRegisteredAt().equals(request.getPurchasedAt())) && (request.getPurchasedAt() == null || existingOrder.getPurchasedAt().equals(request.getPurchasedAt()))
&& sameText(existing.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致"); && sameText(existingOrder.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致");
List<GaoCustomerPackageItem> existingItemList = itemMapper.selectByCustomerPackageId(existing.getId()); List<GaoCustomerPackageItem> existingItemList = itemMapper.selectByCustomerPackageId(existingPackage.getId());
List<GaoCustomerPackagePurchaseItem> requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of()); List<GaoCustomerPackagePurchaseItem> requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of());
verifyPurchaseItems(requestItemList); verifyPurchaseItems(requestItemList);
TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致"); TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致");
Map<String, GaoCustomerPackageItem> existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductNameSnapshot() + "|" + item.getUnitSnapshot(), Function.identity())); Map<String, GaoCustomerPackageItem> existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity()));
for (GaoCustomerPackagePurchaseItem item : requestItemList) { for (GaoCustomerPackagePurchaseItem item : requestItemList) {
GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductName() + "|" + item.getUnit()); GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductId() + "|" + item.getProductUnitId());
TimiException.required(existingItem, "requestNo 对应购买请求内容不一致"); TimiException.required(existingItem, "requestNo 对应购买请求内容不一致");
TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0 TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0
&& existingItem.getSort().equals(item.getSort()) && existingItem.getSort().equals(item.getSort())
@@ -385,14 +404,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
.collect(Collectors.groupingBy(GaoCustomerPackageItem::getCustomerPackageId)); .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) { private String requiredReason(String reason, String message) {
TimiException.required(reason, message); TimiException.required(reason, message);
String result = reason.trim(); String result = reason.trim();
@@ -21,9 +21,14 @@ import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
import com.imyeyu.api.modules.gao.util.EventRecordXSSFBuilder; import com.imyeyu.api.modules.gao.util.EventRecordXSSFBuilder;
import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordNumberValueDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.RoleChecker; import com.imyeyu.api.modules.user.service.RoleChecker;
@@ -94,6 +99,54 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
return mapper.selectAllByExample(example); return mapper.selectAllByExample(example);
} }
@Override
public long countByEventId(String eventId) {
TimiException.required(eventId, "not found eventId");
return mapper.countByEventId(eventId);
}
@Override
public long countByEventIdAndNumberValue(String eventId) {
TimiException.required(eventId, "not found eventId");
return TimiJava.defaultIfNull(mapper.countByEventIdAndNumberValue(eventId), 0L);
}
@Override
public long countByStoreIdAndRegisteredDateValue(String storeId, Integer registeredDateValue) {
TimiException.required(storeId, "not found storeId");
TimiException.required(registeredDateValue, "not found registeredDateValue");
return TimiJava.defaultIfNull(mapper.countByStoreIdAndRegisteredDateValue(storeId, registeredDateValue), 0L);
}
@Override
public void updateBusinessDayNo(String storeId, Integer registeredDateValue, long businessDayNo) {
TimiException.required(storeId, "not found storeId");
TimiException.required(registeredDateValue, "not found registeredDateValue");
mapper.updateBusinessDayNoByStoreIdAndRegisteredDateValue(storeId, registeredDateValue, businessDayNo);
}
@Override
public long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt) {
TimiException.required(storeId, "not found storeId");
TimiException.required(customerId, "not found customerId");
TimiException.required(eventId, "not found eventId");
return TimiJava.defaultIfNull(mapper.count(storeId, customerId, eventId, beginAt, endAt), 0L);
}
@Override
public long sumNumberValue(String storeId, String customerId, String eventId, Long beginAt, Long endAt) {
TimiException.required(storeId, "not found storeId");
TimiException.required(customerId, "not found customerId");
TimiException.required(eventId, "not found eventId");
return TimiJava.defaultIfNull(mapper.sumNumberValue(storeId, customerId, eventId, beginAt, endAt), 0L);
}
@Override
public void updateEarnedPointValue(String id, long earnedPointValue) {
TimiException.required(id, "not found eventRecordId");
mapper.updateEarnedPointValue(id, earnedPointValue);
}
@Override @Override
public void attachSelectedOptionGroupList(List<GaoEventRecord> recordList) { public void attachSelectedOptionGroupList(List<GaoEventRecord> recordList) {
List<String> recordIdList = new ArrayList<>(); List<String> recordIdList = new ArrayList<>();
@@ -161,22 +214,39 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void create(GaoEventRecord record) { public void create(GaoEventRecord record) {
createInternal(record, false); createInternal(record, false, false);
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public GaoEventRecord createOne(GaoEventRecord record) { public GaoEventRecord createOne(GaoEventRecord record) {
return createInternal(record, false); return createInternal(record, false, false);
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public GaoEventRecord createPackagePurchaseRecord(GaoEventRecord record) { public GaoEventRecord createPackagePurchaseRecord(GaoEventRecord record) {
return createInternal(record, true); return createInternal(record, true, false);
} }
private GaoEventRecord createInternal(GaoEventRecord record, boolean packagePurchase) { @Override
public GaoEventRecord createOrderConsumeRecord(GaoEventRecord record) {
return createInternal(record, false, true);
}
@Override
public void revokeOrderConsumeRecord(GaoEventRecord record) {
TimiException.required(record, "not found order event record");
pointTriggerService.revokeByEventRecord(record);
}
@Override
public void rejectMutationIfLinkedOrder(String recordId) {
TimiException.required(recordId, "not found record.id");
TimiException.requiredTrue(mapper.countBySourceEventRecordId(recordId) == 0, "订单关联事件记录不允许修改或删除");
}
private GaoEventRecord createInternal(GaoEventRecord record, boolean packagePurchase, boolean orderConsume) {
TimiException.required(record, "not found record"); TimiException.required(record, "not found record");
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_BATCH_CREATE"); Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_BATCH_CREATE");
@@ -199,7 +269,9 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
verifyRecordValue(event, record.getNumberValue()); verifyRecordValue(event, record.getNumberValue());
verifyRecordAttachment(event, record.getAttachmentList()); verifyRecordAttachment(event, record.getAttachmentList());
// 检查权限 // 检查权限
if (!orderConsume) {
checkEventPermission(event); checkEventPermission(event);
}
int registeredDateValue = dateValue(registeredAt); int registeredDateValue = dateValue(registeredAt);
long businessDayNo = businessDayService.resolveOpenBusinessDay(event.getStoreId(), registeredDateValue).getBusinessDayNo(); long businessDayNo = businessDayService.resolveOpenBusinessDay(event.getStoreId(), registeredDateValue).getBusinessDayNo();
@@ -302,6 +374,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_UPDATE"); Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_UPDATE");
try { try {
logger.setContent(jackson.writeValueAsString(record)); logger.setContent(jackson.writeValueAsString(record));
rejectMutationIfLinkedOrder(record.getId());
GaoEventRecord dbRecord = get(record.getId()); GaoEventRecord dbRecord = get(record.getId());
TimiException.required(dbRecord, "not found event record"); TimiException.required(dbRecord, "not found event record");
TimiException.required(record.getStoreId() == null || record.getStoreId().equals(dbRecord.getStoreId()), "record.storeId invalid"); TimiException.required(record.getStoreId() == null || record.getStoreId().equals(dbRecord.getStoreId()), "record.storeId invalid");
@@ -351,6 +424,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_DELETE"); Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_DELETE");
try { try {
logger.setContent(id); logger.setContent(id);
rejectMutationIfLinkedOrder(id);
GaoEventRecord record = get(id); GaoEventRecord record = get(id);
TimiException.required(record, "not found event record"); TimiException.required(record, "not found event record");
GaoEvent event = eventService.get(record.getEventId()); GaoEvent event = eventService.get(record.getEventId());
@@ -414,6 +488,31 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
return result; return result;
} }
@Override
public List<GaoEventRecordDailyStatView> stateDailyByRange(GaoStoreChartReq req) {
return mapper.stateDailyByRange(req);
}
@Override
public List<GaoEventRecordNumberValueDailyStatView> stateNumberValueDailyByRange(GaoStoreChartReq req) {
return mapper.stateNumberValueDailyByRange(req);
}
@Override
public List<GaoEventRecordCalendarView> calendarByStoreRange(GaoStoreChartReq req) {
return mapper.selectCalendarByRange(req);
}
@Override
public List<GaoEventRecordDailyStatView> stateCustomerDailyByRange(GaoCustomerChartReq req) {
return mapper.stateCustomerDailyByRange(req);
}
@Override
public List<GaoEventRecordCalendarView> calendarByCustomerRange(GaoCustomerChartReq req) {
return mapper.selectCustomerCalendarByRange(req);
}
@Override @Override
public Long sumNumberValueByRange(GaoEventRecordPage page) { public Long sumNumberValueByRange(GaoEventRecordPage page) {
TimiException.required(page.getStoreId(), "not found page.storeId"); TimiException.required(page.getStoreId(), "not found page.storeId");
@@ -6,14 +6,12 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventOption; import com.imyeyu.api.modules.gao.entity.GaoEventOption;
import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup;
import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation; import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.mapper.GaoEventMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventOptionGroupMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventOptionGroupMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventOptionMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventOptionMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventRoleRelationMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventRoleRelationMapper;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoEventService; import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
@@ -26,6 +24,7 @@ import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -57,9 +56,8 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
private final GaoEventMapper mapper; private final GaoEventMapper mapper;
private final GaoEventOptionGroupMapper optionGroupMapper; private final GaoEventOptionGroupMapper optionGroupMapper;
private final GaoEventOptionMapper optionMapper; private final GaoEventOptionMapper optionMapper;
private final GaoEventRecordMapper eventRecordMapper;
private final GaoEventRoleRelationMapper roleRelationMapper; private final GaoEventRoleRelationMapper roleRelationMapper;
private final GaoPackageConfigService packageConfigService; private final ObjectProvider<GaoEventRecordService> eventRecordServiceProvider;
@Override @Override
protected BaseMapper<GaoEvent, String> mapper() { protected BaseMapper<GaoEvent, String> mapper() {
@@ -134,7 +132,7 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
TimiException.requiredTrue(bizType == TimiJava.defaultIfNull(dbEvent.getBizType(), GaoEvent.BizType.GENERAL), "事件业务类型创建后不可修改"); TimiException.requiredTrue(bizType == TimiJava.defaultIfNull(dbEvent.getBizType(), GaoEvent.BizType.GENERAL), "事件业务类型创建后不可修改");
dbEvent.setBizType(bizType); dbEvent.setBizType(bizType);
if (event.getPackageConfigId() != null && !event.getPackageConfigId().equals(dbEvent.getPackageConfigId())) { if (event.getPackageConfigId() != null && !event.getPackageConfigId().equals(dbEvent.getPackageConfigId())) {
TimiException.requiredTrue(0 == eventRecordMapper.countByEventId(dbEvent.getId()), "已有登记记录的事件不能更换绑定套餐"); TimiException.requiredTrue(0 == eventRecordServiceProvider.getObject().countByEventId(dbEvent.getId()), "已有登记记录的事件不能更换绑定套餐");
dbEvent.setPackageConfigId(event.getPackageConfigId()); dbEvent.setPackageConfigId(event.getPackageConfigId());
} }
dbEvent.setValueType(TimiJava.defaultIfNull(event.getValueType(), oldValueType)); dbEvent.setValueType(TimiJava.defaultIfNull(event.getValueType(), oldValueType));
@@ -256,6 +254,12 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
return mapper.selectByIdList(new HashSet<>(idList)).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity())); return mapper.selectByIdList(new HashSet<>(idList)).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity()));
} }
@Override
public long countByPackageConfigId(String packageConfigId) {
TimiException.required(packageConfigId, "not found packageConfigId");
return mapper.countByPackageConfigId(packageConfigId);
}
@Override @Override
public Map<String, List<String>> mapRoleCodeListByEventIdList(Collection<String> eventIdList) { public Map<String, List<String>> mapRoleCodeListByEventIdList(Collection<String> eventIdList) {
return roleRelationMapper.selectAllByEventIdList(new HashSet<>(eventIdList)) return roleRelationMapper.selectAllByEventIdList(new HashSet<>(eventIdList))
@@ -325,9 +329,6 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
if (event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE) { if (event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE) {
TimiException.requiredTrue(!Boolean.TRUE.equals(event.getRequireVerification()), "购买套餐事件不能要求核销"); TimiException.requiredTrue(!Boolean.TRUE.equals(event.getRequireVerification()), "购买套餐事件不能要求核销");
TimiException.required(event.getPackageConfigId(), "购买套餐事件必须绑定套餐配置"); TimiException.required(event.getPackageConfigId(), "购买套餐事件必须绑定套餐配置");
GaoPackageConfig packageConfig = packageConfigService.get(event.getPackageConfigId());
TimiException.required(packageConfig, "购买套餐事件绑定的套餐配置不存在");
TimiException.requiredTrue(event.getStoreId().equals(packageConfig.getStoreId()), "购买事件和套餐配置不属于同一门店");
} else { } else {
TimiException.requiredNull(event.getPackageConfigId(), "普通事件不能绑定套餐配置"); TimiException.requiredNull(event.getPackageConfigId(), "普通事件不能绑定套餐配置");
} }
@@ -450,8 +451,8 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
if (oldValueType == event.getValueType() && oldValueUnit == event.getValueUnit()) { if (oldValueType == event.getValueType() && oldValueUnit == event.getValueUnit()) {
return; return;
} }
Long count = eventRecordMapper.countByEventIdAndNumberValue(event.getId()); long count = eventRecordServiceProvider.getObject().countByEventIdAndNumberValue(event.getId());
if (0 < TimiJava.defaultIfNull(count, 0L)) { if (0 < count) {
TimiException.requiredTrue(oldValueUnit == event.getValueUnit(), "事件已有数值登记记录,禁止修改 valueUnit"); TimiException.requiredTrue(oldValueUnit == event.getValueUnit(), "事件已有数值登记记录,禁止修改 valueUnit");
TimiException.requiredTrue(event.getValueType() == GaoEvent.ValueType.NUMBER, "事件已有数值登记记录,不能改为 NONE"); TimiException.requiredTrue(event.getValueType() == GaoEvent.ValueType.NUMBER, "事件已有数值登记记录,不能改为 NONE");
} }
@@ -1,63 +1,45 @@
package com.imyeyu.api.modules.gao.service.implement; 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.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.mapper.GaoFundRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoFundRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService; 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.GaoFundActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
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.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
/// GAO 通用资金流水服务实现 /// GAO 销售订单资金流水服务实现
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoFundRecordServiceImplement extends AbstractEntityService<GaoFundRecord, String> implements GaoFundRecordService { public class GaoFundRecordServiceImplement extends AbstractEntityService<GaoFundRecord, String> implements GaoFundRecordService {
private final GaoFundRecordMapper mapper; 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 @Override
protected BaseMapper<GaoFundRecord, String> mapper() { protected BaseMapper<GaoFundRecord, String> mapper() {
return mapper; return mapper;
} }
/// 资金流水必须通过专用操作创建 /// 资金流水必须通过销售订单资金接口创建
/// ///
/// @param record 资金流水 /// @param record 资金流水
@Override @Override
public void create(GaoFundRecord record) { public void create(GaoFundRecord record) {
TimiException.requiredTrue(false, "资金流水必须通过专用接口创建"); TimiException.requiredTrue(false, "资金流水必须通过销售订单资金接口创建");
}
@Override
public void createByOrderPayment(GaoFundRecord record) {
TimiException.required(record, "not found fund record");
super.create(record);
} }
/// 资金流水不允许通用修改 /// 资金流水不允许通用修改
@@ -94,296 +76,41 @@ public class GaoFundRecordServiceImplement extends AbstractEntityService<GaoFund
} }
@Override @Override
public List<GaoFundRecord> listByCustomerPackageId(String customerPackageId) { public List<GaoFundRecord> listByBizId(String salesOrderId) {
TimiException.required(customerPackageId, "not found customerPackageId"); TimiException.required(salesOrderId, "not found salesOrderId");
return mapper.selectByBizId(customerPackageId); return mapper.selectByBizId(salesOrderId);
} }
@Override @Override
public long countPendingReceiveByBizId(String bizId) { public long countPendingReceiveByBizId(String salesOrderId) {
return mapper.countPendingReceiveByBizId(bizId); TimiException.required(salesOrderId, "not found salesOrderId");
return mapper.countPendingReceiveByBizId(salesOrderId);
} }
@Override @Override
public GaoFundRecord get(String id) { public GaoFundRecord getByRequestNo(String storeId, String requestNo) {
return super.get(id); TimiException.required(storeId, "not found storeId");
TimiException.required(requestNo, "not found requestNo");
return mapper.selectByRequestNo(storeId, requestNo);
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public GaoFundRecord receive(GaoFundReceiveRequest request) { public GaoFundRecord getForUpdate(String id) {
return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CREATE", request, () -> receiveInternal(request)); TimiException.required(id, "not found fundRecordId");
return mapper.selectForUpdate(id);
} }
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 @Override
public GaoFundRecord verify(String fundRecordId) { public GaoFundRecord getRevokeBySourceRecordId(String sourceRecordId) {
return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_VERIFY", fundRecordId, () -> verifyInternal(fundRecordId)); TimiException.required(sourceRecordId, "not found sourceRecordId");
return mapper.selectRevokeBySourceRecordId(sourceRecordId);
} }
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 @Override
public GaoFundRecord reject(GaoFundActionRequest request) { public int updateStatusIf(String id, GaoFundRecord.Status fromStatus, GaoFundRecord.Status toStatus, String verifierUserId, Long verifiedAt, String reason) {
return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_REJECT", request, () -> releasePending(request, GaoFundRecord.Status.REJECTED)); TimiException.required(id, "not found fundRecordId");
} TimiException.required(fromStatus, "not found fromStatus");
TimiException.required(toStatus, "not found toStatus");
@Transactional(TimiServerDBConfig.ROLLBACKER) return mapper.updateStatusIf(id, fromStatus, toStatus, verifierUserId, verifiedAt, reason);
@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<Attachment> 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;
} }
} }
@@ -0,0 +1,695 @@
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.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.api.modules.gao.entity.GaoStockRecord;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillmentItem;
import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentItemMapper;
import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductStockService;
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.service.GaoStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.RoleChecker;
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.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/// GAO 统一履约服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
@RequiredArgsConstructor
public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<GaoOrderFulfillment, String> implements GaoOrderFulfillmentService {
private static final BigDecimal ZERO = BigDecimal.ZERO;
private final GaoOrderFulfillmentMapper mapper;
private final GaoOrderFulfillmentItemMapper itemMapper;
private final GaoSalesOrderService salesOrderService;
private final GaoCustomerPackageService customerPackageService;
private final GaoProductService productService;
private final GaoProductUnitService unitService;
private final GaoProductStockService productStockService;
private final GaoStockRecordService stockRecordService;
private final GaoStoreScopeService storeScopeService;
private final RoleChecker roleChecker;
private final UserLoginService userLoginService;
private final AttachmentService attachmentService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoOrderFulfillment, String> mapper() {
return mapper;
}
/// 履约单必须通过专用流程创建
@Override
public void create(GaoOrderFulfillment fulfillment) {
throw new TimiException(TimiCode.ERROR, "履约单必须通过专用接口创建");
}
/// 履约单不允许通用修改
@Override
public void update(GaoOrderFulfillment fulfillment) {
throw new TimiException(TimiCode.ERROR, "履约单不允许通用修改");
}
/// 履约单不允许删除
@Override
public void delete(String id) {
throw new TimiException(TimiCode.ERROR, "履约单不允许删除");
}
/// 履约单不允许物理删除
@Override
public void destroy(String id) {
throw new TimiException(TimiCode.ERROR, "履约单不允许物理删除");
}
@Override
public PageResult<GaoOrderFulfillment> pageByQuery(GaoOrderFulfillmentPage page) {
TimiException.required(page, "not found fulfillment page");
PageResult<GaoOrderFulfillment> result = new PageResult<>();
result.setTotal(mapper.countByQuery(page));
List<GaoOrderFulfillment> list = mapper.selectByQuery(page);
fillItemList(list);
result.setList(list);
return result;
}
@Override
public GaoOrderFulfillment detail(String id) {
GaoOrderFulfillment fulfillment = mapper.selectById(id);
TimiException.required(fulfillment, "not found fulfillment");
storeScopeService.checkStoreId(fulfillment.getStoreId());
fulfillment.setItemList(itemMapper.selectByFulfillmentId(fulfillment.getId()));
fulfillment.setStockRecordList(stockRecordService.listByBiz(GaoStockRecord.BizType.ORDER_FULFILLMENT, fulfillment.getId()));
return fulfillment;
}
@Override
public List<GaoOrderFulfillment> listBySalesOrderId(String salesOrderId) {
TimiException.required(salesOrderId, "not found salesOrderId");
List<GaoOrderFulfillment> list = mapper.selectBySalesOrderId(salesOrderId);
fillItemList(list);
return list;
}
@Override
public long countPendingBySalesOrderId(String salesOrderId) {
TimiException.required(salesOrderId, "not found salesOrderId");
return mapper.countPendingBySalesOrderId(salesOrderId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CREATE", request, () -> createInternal(request));
}
private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) {
TimiException.required(request, "not found fulfillment request");
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
TimiException.required(request.getSalesOrderId(), "not found salesOrderId");
GaoSalesOrder order = salesOrderService.getForUpdate(request.getSalesOrderId());
GaoOrderFulfillment existing = mapper.selectByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
verifyIdempotency(existing, request);
return detail(existing.getId());
}
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "履约必须上传照片");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能履约");
salesOrderService.checkFulfillmentPermission(order);
GaoCustomerPackage packageEntity = lockPackage(order, true);
List<SourceEntry> sourceList = loadSources(order, packageEntity, requestItems(request.getItemList()), true);
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
boolean stockReady = stockItems.isEmpty() || productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems);
String operatorUserId = userLoginService.getRequireLoginUserId();
GaoOrderFulfillment fulfillment = newFulfillment(order, sourceList.getFirst().packageSource()
? GaoOrderFulfillment.FulfillmentType.PACKAGE_ENTITLEMENT : GaoOrderFulfillment.FulfillmentType.DIRECT_PRODUCT,
stockReady ? GaoOrderFulfillment.Status.PENDING : GaoOrderFulfillment.Status.WAITING_STOCK,
requestNo, operatorUserId, TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()), request.getRemark());
super.create(fulfillment);
insertSourceReservations(fulfillment, sourceList);
if (stockReady && !stockItems.isEmpty()) {
productStockService.reserveForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems,
stockRequestNo(fulfillment, "RESERVE"), operatorUserId);
}
attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, fulfillment.getId(), request.getAttachmentList());
return detail(fulfillment.getId());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_PREPARE_STOCK", request, () -> prepareStockInternal(request));
}
private GaoOrderFulfillment prepareStockInternal(GaoOrderFulfillmentPrepareStockRequest request) {
TimiException.required(request, "not found prepare stock request");
TimiException.required(request.getFulfillmentId(), "not found fulfillmentId");
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId());
TimiException.required(fulfillment, "not found fulfillment");
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态");
GaoSalesOrder order = salesOrderService.getForUpdate(fulfillment.getSalesOrderId());
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存");
salesOrderService.checkFulfillmentPermission(order);
GaoCustomerPackage packageEntity = lockPackage(order, false);
List<SourceEntry> sourceList = loadSources(order, packageEntity, copyRequestItems(itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId())), false);
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
TimiException.requiredTrue(!stockItems.isEmpty(), "履约单没有需要占用的实物库存");
TimiException.requiredTrue(productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems), "库存仍然不足,不能准备履约");
String operatorUserId = userLoginService.getRequireLoginUserId();
productStockService.reserveForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems,
stockRequestNo(fulfillment, "RESERVE"), operatorUserId);
TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), GaoOrderFulfillment.Status.PENDING, null, null, request.getReason()), "履约状态已变化");
if (request.getAttachmentList() != null && !request.getAttachmentList().isEmpty()) {
attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, fulfillment.getId(), request.getAttachmentList());
}
return detail(fulfillment.getId());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment verify(GaoOrderFulfillmentActionRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_VERIFY", request, () -> verifyInternal(request));
}
private GaoOrderFulfillment verifyInternal(GaoOrderFulfillmentActionRequest request) {
TimiException.required(request, "not found verify request");
TimiException.required(request.getFulfillmentId(), "not found fulfillmentId");
GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId());
TimiException.required(fulfillment, "not found fulfillment");
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态");
GaoSalesOrder order = salesOrderService.getForUpdate(fulfillment.getSalesOrderId());
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约");
salesOrderService.checkFulfillmentPermission(order);
GaoCustomerPackage packageEntity = lockPackage(order, false);
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
List<SourceEntry> sourceList = loadSources(order, packageEntity, copyRequestItems(fulfillmentItemList), false);
assertReserved(sourceList);
for (SourceEntry source : sourceList) {
decreaseReserved(source);
increaseFulfilled(source);
}
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
String verifierUserId = userLoginService.getRequireLoginUserId();
if (!stockItems.isEmpty()) {
productStockService.outForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems,
stockRequestNo(fulfillment, "OUT"), verifierUserId);
}
TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), GaoOrderFulfillment.Status.VERIFIED,
verifierUserId, Time.now(), request.getReason()), "履约状态已变化");
refreshAggregateStatus(order, packageEntity);
return detail(fulfillment.getId());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment reject(GaoOrderFulfillmentActionRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REJECT", request, () -> releaseInternal(request, GaoOrderFulfillment.Status.REJECTED));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment cancel(GaoOrderFulfillmentActionRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CANCEL", request, () -> cancelInternal(request));
}
private GaoOrderFulfillment cancelInternal(GaoOrderFulfillmentActionRequest request) {
TimiException.required(request, "not found cancel request");
GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId());
TimiException.required(fulfillment, "not found fulfillment");
String loginUserId = userLoginService.getRequireLoginUserId();
TimiException.requiredTrue(loginUserId.equals(fulfillment.getOperatorUserId()), "只能撤销自己提交的履约");
return releaseInternal(request, GaoOrderFulfillment.Status.CANCELLED);
}
private GaoOrderFulfillment releaseInternal(GaoOrderFulfillmentActionRequest request, GaoOrderFulfillment.Status targetStatus) {
TimiException.required(request, "not found action request");
TimiException.required(request.getFulfillmentId(), "not found fulfillmentId");
String reason = requiredText(request.getReason(), "拒绝或撤销原因不能为空");
GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId());
TimiException.required(fulfillment, "not found fulfillment");
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK
|| fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是可释放状态");
GaoSalesOrder order = salesOrderService.getForUpdate(fulfillment.getSalesOrderId());
GaoCustomerPackage packageEntity = lockPackage(order, false);
List<SourceEntry> sourceList = loadSources(order, packageEntity, copyRequestItems(itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId())), false);
if (fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING) {
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
if (!stockItems.isEmpty()) {
productStockService.releaseForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems,
stockRequestNo(fulfillment, "RELEASE"), userLoginService.getRequireLoginUserId());
}
}
for (SourceEntry source : sourceList) {
decreaseReserved(source);
}
String operatorUserId = userLoginService.getRequireLoginUserId();
TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), targetStatus, operatorUserId, Time.now(), reason), "履约状态已变化");
return detail(fulfillment.getId());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment revoke(GaoOrderFulfillmentRevokeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REVOKE", request, () -> revokeInternal(request));
}
private GaoOrderFulfillment revokeInternal(GaoOrderFulfillmentRevokeRequest request) {
TimiException.required(request, "not found revoke request");
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
TimiException.required(request.getFulfillmentId(), "not found fulfillmentId");
String reason = requiredText(request.getReason(), "冲正原因不能为空");
GaoOrderFulfillment source = mapper.selectForUpdate(request.getFulfillmentId());
TimiException.required(source, "not found fulfillment");
storeScopeService.checkStoreId(source.getStoreId());
GaoOrderFulfillment existing = mapper.selectByRequestNo(source.getStoreId(), requestNo);
if (existing != null) {
TimiException.requiredTrue(source.getId().equals(existing.getSourceFulfillmentId()), "requestNo 已被其他履约操作使用");
return detail(existing.getId());
}
TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正");
TimiException.requiredNull(mapper.selectBySourceFulfillmentId(source.getId()), "该履约单已经冲正");
GaoSalesOrder order = salesOrderService.getForUpdate(source.getSalesOrderId());
GaoCustomerPackage packageEntity = lockPackage(order, false);
List<GaoOrderFulfillmentItem> sourceItemList = itemMapper.selectByFulfillmentIdForUpdate(source.getId());
List<SourceEntry> sourceList = loadSources(order, packageEntity, copyRequestItems(sourceItemList), false);
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
if (!stockItems.isEmpty()) {
TimiException.requiredTrue(Boolean.TRUE.equals(request.getReturnedToStore()), "实物履约冲正必须确认已退回门店");
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "实物履约冲正必须上传退回照片");
}
String operatorUserId = userLoginService.getRequireLoginUserId();
GaoOrderFulfillment reverse = newFulfillment(order, source.getFulfillmentType(), GaoOrderFulfillment.Status.VERIFIED,
requestNo, operatorUserId, TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()), request.getRemark());
reverse.setVerifierUserId(operatorUserId);
reverse.setVerifiedAt(Time.now());
reverse.setSourceFulfillmentId(source.getId());
reverse.setReason(reason);
super.create(reverse);
for (int i = 0; i < sourceList.size(); i++) {
SourceEntry sourceEntry = sourceList.get(i);
decreaseFulfilled(sourceEntry);
GaoOrderFulfillmentItem sourceItem = sourceItemList.get(i);
GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem);
reverseItem.setFulfillmentId(reverse.getId());
reverseItem.setReservedBefore(sourceEntry.reservedBefore());
reverseItem.setReservedAfter(sourceEntry.reservedBefore());
reverseItem.setFulfilledBefore(sourceEntry.fulfilledBefore());
reverseItem.setFulfilledAfter(sourceEntry.fulfilledBefore().subtract(sourceEntry.quantity()));
itemMapper.insert(reverseItem);
}
if (!stockItems.isEmpty()) {
for (GaoStockChangeItem item : stockItems) {
GaoStockRecord stockRecord = stockRecordService.getByBizProduct(GaoStockRecord.BizType.ORDER_FULFILLMENT,
source.getId(), item.getProductId(), GaoStockRecord.OperationType.ORDER_OUT);
if (stockRecord != null) {
item.setSourceRecordId(stockRecord.getId());
}
}
productStockService.revokeForFulfillment(order.getStoreId(), reverse.getId(), stockItems,
stockRequestNo(reverse, "REVOKE"), operatorUserId);
}
TimiException.requiredTrue(1 == mapper.updateStatus(source.getId(), GaoOrderFulfillment.Status.REVOKED,
operatorUserId, Time.now(), reason), "原履约状态已变化");
attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, reverse.getId(), request.getAttachmentList());
refreshAggregateStatus(order, packageEntity);
return detail(reverse.getId());
}
private GaoOrderFulfillment newFulfillment(GaoSalesOrder order, GaoOrderFulfillment.FulfillmentType type,
GaoOrderFulfillment.Status status, String requestNo, String operatorUserId, Long deliveredAt, String remark) {
GaoOrderFulfillment result = new GaoOrderFulfillment();
result.setSalesOrderId(order.getId());
result.setStoreId(order.getStoreId());
result.setCustomerId(order.getCustomerId());
result.setFulfillmentType(type);
result.setStatus(status);
result.setRequestNo(requestNo);
result.setOperatorUserId(operatorUserId);
result.setDeliveredAt(deliveredAt);
result.setRemark(remark);
return result;
}
private void insertSourceReservations(GaoOrderFulfillment fulfillment, List<SourceEntry> sourceList) {
for (SourceEntry source : sourceList) {
if (source.packageSource()) {
TimiException.requiredTrue(1 == customerPackageService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "套餐权益占用失败");
} else {
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "订单明细占用失败");
}
GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId());
itemMapper.insert(item);
}
}
private void decreaseReserved(SourceEntry source) {
if (source.packageSource()) {
TimiException.requiredTrue(1 == customerPackageService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "套餐权益占用释放失败");
} else {
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "订单明细占用释放失败");
}
}
private void increaseFulfilled(SourceEntry source) {
if (source.packageSource()) {
TimiException.requiredTrue(1 == customerPackageService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "套餐权益履约失败");
} else {
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "订单明细履约失败");
}
}
private void decreaseFulfilled(SourceEntry source) {
TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正");
if (source.packageSource()) {
TimiException.requiredTrue(1 == customerPackageService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "套餐权益冲正失败");
} else {
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败");
}
}
private void assertReserved(List<SourceEntry> sourceList) {
for (SourceEntry source : sourceList) {
TimiException.requiredTrue(0 <= source.reservedBefore().compareTo(source.quantity()), "履约待占用数量不足");
}
}
private List<SourceEntry> loadSources(GaoSalesOrder order, GaoCustomerPackage packageEntity,
List<GaoOrderFulfillmentItemRequest> requestItems, boolean checkAvailable) {
TimiException.requiredTrue(requestItems != null && !requestItems.isEmpty(), "履约至少需要一个产品");
Set<String> sourceIdSet = new HashSet<>();
List<SourceEntry> result = new ArrayList<>();
for (GaoOrderFulfillmentItemRequest requestItem : requestItems) {
TimiException.required(requestItem, "履约明细不能为空");
boolean hasOrderSource = hasText(requestItem.getSourceOrderItemId());
boolean hasPackageSource = hasText(requestItem.getSourceCustomerPackageItemId());
TimiException.requiredTrue(hasOrderSource ^ hasPackageSource, "履约明细来源必须二选一");
String sourceId = hasOrderSource ? requestItem.getSourceOrderItemId() : requestItem.getSourceCustomerPackageItemId();
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
TimiException.required(requestItem.getQuantity(), "履约数量不能为空");
TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0");
TimiException.requiredTrue(3 >= requestItem.getQuantity().stripTrailingZeros().scale(), "履约数量最多保留三位小数");
if (order.getOrderType() == GaoSalesOrder.OrderType.DIRECT_PRODUCT) {
TimiException.requiredTrue(hasOrderSource && !hasPackageSource, "直接产品订单只能引用订单明细");
GaoSalesOrderItem source = salesOrderService.getItemForUpdate(sourceId);
TimiException.required(source, "not found sales order item");
TimiException.requiredTrue(order.getId().equals(source.getSalesOrderId()), "订单明细不属于当前订单");
result.add(toSource(order, source, requestItem.getQuantity(), false, checkAvailable));
} else {
TimiException.requiredTrue(hasPackageSource && !hasOrderSource, "套餐订单只能引用客户套餐权益");
TimiException.required(packageEntity, "订单未找到客户套餐权益");
GaoCustomerPackageItem source = customerPackageService.getItemForUpdate(sourceId);
TimiException.required(source, "not found customer package item");
TimiException.requiredTrue(packageEntity.getId().equals(source.getCustomerPackageId()), "套餐权益不属于当前订单");
result.add(toSource(order, source, requestItem.getQuantity(), true, checkAvailable));
}
}
return result;
}
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean packageSource, boolean checkAvailable) {
GaoProduct product = productService.get(source.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal conversion = requiredConversion(source.getConversionToBaseSnapshot());
GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), conversion);
return buildSource(order, source.getId(), null, source.getProductId(), source.getProductUnitId(), source.getProductNameSnapshot(),
source.getUnitSnapshot(), conversion, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(),
product, unit, packageSource, checkAvailable);
}
private SourceEntry toSource(GaoSalesOrder order, GaoCustomerPackageItem source, BigDecimal quantity, boolean packageSource, boolean checkAvailable) {
TimiException.required(source.getProductId(), "套餐权益缺少产品快照,不能履约");
TimiException.required(source.getProductUnitId(), "套餐权益缺少单位快照,不能履约");
GaoProduct product = productService.get(source.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal conversion = requiredConversion(source.getConversionToBaseSnapshot());
GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), conversion);
return buildSource(order, null, source.getId(), source.getProductId(), source.getProductUnitId(), source.getProductNameSnapshot(),
source.getUnitSnapshot(), conversion, quantity, source.getTotalQuantity(), source.getReservedQuantity(), fulfilledQuantity(source),
product, unit, packageSource, checkAvailable);
}
private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String packageItemId, String productId, String unitId,
String productName, String unitName, BigDecimal conversion, BigDecimal quantity, BigDecimal totalQuantity,
BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductUnit unit,
boolean packageSource, boolean checkAvailable) {
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
}
BigDecimal reservedBefore = decimal(reservedQuantity);
BigDecimal fulfilledBefore = decimal(fulfilledQuantity);
if (checkAvailable) {
BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore);
TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足");
}
return new SourceEntry(orderItemId, packageItemId, productId, unitId, productName, unitName, conversion,
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, unit, packageSource);
}
private List<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) {
Map<String, GaoStockChangeItem> itemMap = new LinkedHashMap<>();
for (SourceEntry source : sourceList) {
if (!Boolean.TRUE.equals(source.product().getStockEnabled())) {
continue;
}
GaoProductUnit baseUnit = productService.listUnitByProductId(source.product().getId()).stream()
.filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null);
TimiException.required(baseUnit, "实物产品缺少基础库存单位");
BigDecimal baseQuantity = unitService.convertToBase(source.unit(), source.quantity());
GaoStockChangeItem item = itemMap.get(source.product().getId());
if (item == null) {
item = GaoStockChangeItem.of(source.product(), baseUnit, baseQuantity, baseQuantity);
itemMap.put(source.product().getId(), item);
} else {
item.setQuantity(item.getQuantity().add(baseQuantity));
item.setBaseQuantity(item.getBaseQuantity().add(baseQuantity));
}
}
return itemMap.values().stream().sorted(Comparator.comparing(GaoStockChangeItem::getProductId)).toList();
}
private GaoCustomerPackage lockPackage(GaoSalesOrder order, boolean requireActive) {
if (order.getOrderType() != GaoSalesOrder.OrderType.PACKAGE) {
return null;
}
GaoCustomerPackage source = customerPackageService.getBySalesOrderId(order.getId());
TimiException.required(source, "订单未找到客户套餐权益");
GaoCustomerPackage result = customerPackageService.getForUpdate(source.getId());
TimiException.required(result, "not found customer package");
if (requireActive) {
TimiException.requiredTrue(result.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可履约");
} else {
TimiException.requiredTrue(result.getStatus() != GaoCustomerPackage.Status.CANCELLED, "已取消套餐不能履约");
}
return result;
}
private BigDecimal fulfilledQuantity(GaoCustomerPackageItem item) {
return item.getFulfilledQuantity();
}
private void refreshAggregateStatus(GaoSalesOrder order, GaoCustomerPackage packageEntity) {
if (packageEntity != null) {
List<GaoCustomerPackageItem> itemList = customerPackageService.listItemByCustomerPackageId(packageEntity.getId());
boolean fulfilled = !itemList.isEmpty();
for (GaoCustomerPackageItem item : itemList) {
if (decimal(fulfilledQuantity(item)).compareTo(decimal(item.getTotalQuantity())) < 0) {
fulfilled = false;
break;
}
}
if (fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) {
packageEntity.setStatus(GaoCustomerPackage.Status.FULFILLED);
customerPackageService.updateAggregateState(packageEntity);
} else if (!fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.FULFILLED) {
packageEntity.setStatus(GaoCustomerPackage.Status.ACTIVE);
customerPackageService.updateAggregateState(packageEntity);
}
salesOrderService.updateFulfillmentStatus(order, fulfilled);
} else {
salesOrderService.refreshFulfillmentStatus(order);
}
}
private List<GaoOrderFulfillmentItemRequest> requestItems(List<GaoOrderFulfillmentItemRequest> itemList) {
return itemList;
}
private List<GaoOrderFulfillmentItemRequest> copyRequestItems(List<GaoOrderFulfillmentItem> itemList) {
List<GaoOrderFulfillmentItemRequest> result = new ArrayList<>();
for (GaoOrderFulfillmentItem item : TimiJava.safeIterable(itemList)) {
GaoOrderFulfillmentItemRequest request = new GaoOrderFulfillmentItemRequest();
request.setSourceOrderItemId(item.getSourceOrderItemId());
request.setSourceCustomerPackageItemId(item.getSourceCustomerPackageItemId());
request.setQuantity(item.getQuantity());
result.add(request);
}
return result;
}
private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setProductId(source.getProductId());
result.setProductUnitId(source.getProductUnitId());
result.setSourceOrderItemId(source.getSourceOrderItemId());
result.setSourceCustomerPackageItemId(source.getSourceCustomerPackageItemId());
result.setQuantity(source.getQuantity());
result.setBaseQuantity(source.getBaseQuantity());
result.setProductNameSnapshot(source.getProductNameSnapshot());
result.setUnitSnapshot(source.getUnitSnapshot());
result.setConversionToBaseSnapshot(source.getConversionToBaseSnapshot());
return result;
}
private GaoProductUnit snapshotUnit(String id, String name, BigDecimal conversion) {
GaoProductUnit unit = new GaoProductUnit();
unit.setId(id);
unit.setName(name);
unit.setConversionToBase(conversion);
return unit;
}
private BigDecimal requiredConversion(BigDecimal value) {
TimiException.required(value, "履约来源缺少折算率快照");
TimiException.requiredTrue(0 < value.signum(), "履约来源折算率无效");
return value;
}
private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) {
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
&& sameText(existing.getRemark(), request.getRemark()), "requestNo 对应履约请求内容不一致");
List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId());
List<GaoOrderFulfillmentItemRequest> requestItems = requestItems(request.getItemList());
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
Map<String, BigDecimal> requestMap = new HashMap<>();
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) {
String key = hasText(item.getSourceOrderItemId()) ? "O:" + item.getSourceOrderItemId() : "P:" + item.getSourceCustomerPackageItemId();
requestMap.put(key, item.getQuantity());
}
for (GaoOrderFulfillmentItem item : existingItems) {
String key = item.getSourceOrderItemId() == null ? "P:" + item.getSourceCustomerPackageItemId() : "O:" + item.getSourceOrderItemId();
TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(key, ZERO)) == 0, "requestNo 对应履约请求内容不一致");
}
}
private String stockRequestNo(GaoOrderFulfillment fulfillment, String operation) {
return "FULFILLMENT-%s-%s".formatted(fulfillment.getId(), operation);
}
private boolean hasAttachment(List<Attachment> attachmentList) {
for (Attachment attachment : TimiJava.safeIterable(attachmentList)) {
if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) {
return true;
}
}
return false;
}
private boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
private String requiredText(String value, String message) {
TimiException.required(value, message);
String result = value.trim();
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
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;
}
private void fillItemList(List<GaoOrderFulfillment> list) {
for (GaoOrderFulfillment fulfillment : TimiJava.safeIterable(list)) {
fulfillment.setItemList(itemMapper.selectByFulfillmentId(fulfillment.getId()));
}
}
private record SourceEntry(String sourceOrderItemId, String sourceCustomerPackageItemId, String productId, String productUnitId,
String productNameSnapshot, String unitSnapshot, BigDecimal conversionToBaseSnapshot, BigDecimal quantity,
BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product,
GaoProductUnit unit, boolean packageSource) {
private String sourceId() {
return packageSource ? sourceCustomerPackageItemId : sourceOrderItemId;
}
private GaoOrderFulfillmentItem toFulfillmentItem(String fulfillmentId) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setFulfillmentId(fulfillmentId);
result.setProductId(productId);
result.setProductUnitId(productUnitId);
result.setSourceOrderItemId(sourceOrderItemId);
result.setSourceCustomerPackageItemId(sourceCustomerPackageItemId);
result.setQuantity(quantity);
result.setBaseQuantity(quantity.multiply(conversionToBaseSnapshot).setScale(3));
result.setReservedBefore(reservedBefore);
result.setReservedAfter(reservedBefore.add(quantity));
result.setFulfilledBefore(fulfilledBefore);
result.setFulfilledAfter(fulfilledBefore);
result.setProductNameSnapshot(productNameSnapshot);
result.setUnitSnapshot(unitSnapshot);
result.setConversionToBaseSnapshot(conversionToBaseSnapshot);
return result;
}
}
}
@@ -0,0 +1,280 @@
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.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
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.TimiException;
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.util.List;
/// GAO 销售订单资金服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
@RequiredArgsConstructor
public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
private final GaoFundRecordService fundRecordService;
private final GaoSalesOrderService orderService;
private final AttachmentService attachmentService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord receive(GaoOrderPaymentReceiveRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_RECEIVE", request, () -> receiveInternal(request));
}
private GaoFundRecord receiveInternal(GaoOrderPaymentReceiveRequest request) {
TimiException.required(request, "not found order payment request");
TimiException.required(request.getSalesOrderId(), "not found salesOrderId");
TimiException.required(request.getAmount(), "收款金额不能为空");
TimiException.requiredTrue(0 < request.getAmount(), "收款金额必须大于 0");
GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId());
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
verifySameReceive(existing, order, request);
return existing;
}
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能收款");
TimiException.requiredTrue(order.getRefundStatus() != GaoSalesOrder.RefundStatus.REFUNDED, "已完成退款订单不能收款");
long pending = safe(order.getPendingPaidAmount());
long paid = safe(order.getPaidAmount());
TimiException.requiredTrue(pending + paid + request.getAmount() <= safe(order.getSaleAmount()), "累计收款不能超过成交价");
TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "收款必须上传照片");
GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.RECEIVE, requestNo, request.getAmount(), request.getOccurredAt(), request.getRemark());
record.setStatus(GaoFundRecord.Status.PENDING);
fundRecordService.createByOrderPayment(record);
order.setPendingPaidAmount(pending + request.getAmount());
orderService.recomputePaymentStatus(order);
orderService.updatePayment(order);
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_SALES_ORDER_PAYMENT_VERIFY", fundRecordId, () -> {
GaoFundRecord record = lockOrderRecord(fundRecordId);
TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态");
GaoSalesOrder order = orderService.getForUpdate(record.getBizId());
order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount());
order.setPaidAmount(safe(order.getPaidAmount()) + record.getAmount());
orderService.recomputePaymentStatus(order);
String userId = userLoginService.getRequireLoginUserId();
TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化");
orderService.updatePayment(order);
record.setStatus(GaoFundRecord.Status.VERIFIED);
record.setVerifierUserId(userId);
record.setVerifiedAt(Time.now());
return record;
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord reject(GaoOrderPaymentActionRequest request) {
return releasePending(request, GaoFundRecord.Status.REJECTED, false);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord cancel(GaoOrderPaymentActionRequest request) {
return releasePending(request, GaoFundRecord.Status.CANCELLED, true);
}
private GaoFundRecord releasePending(GaoOrderPaymentActionRequest request, GaoFundRecord.Status targetStatus, boolean onlyOperator) {
TimiException.required(request, "not found order payment action request");
TimiException.required(request.getFundRecordId(), "not found fundRecordId");
String reason = requiredText(request.getReason(), "资金操作原因不能为空");
GaoFundRecord record = lockOrderRecord(request.getFundRecordId());
TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态");
if (onlyOperator) {
TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(record.getOperatorUserId()), "只能撤销自己提交的收款");
}
GaoSalesOrder order = orderService.getForUpdate(record.getBizId());
order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount());
orderService.recomputePaymentStatus(order);
String userId = userLoginService.getRequireLoginUserId();
TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, targetStatus, userId, Time.now(), reason), "收款记录状态已变化");
orderService.updatePayment(order);
record.setStatus(targetStatus);
record.setReason(reason);
record.setVerifierUserId(userId);
record.setVerifiedAt(Time.now());
return record;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord refund(GaoOrderPaymentRefundRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_REFUND", request, () -> refundInternal(request));
}
private GaoFundRecord refundInternal(GaoOrderPaymentRefundRequest request) {
TimiException.required(request, "not found refund request");
TimiException.required(request.getSalesOrderId(), "not found salesOrderId");
TimiException.required(request.getAmount(), "退款金额不能为空");
TimiException.requiredTrue(0 < request.getAmount(), "退款金额必须大于 0");
String reason = requiredText(request.getReason(), "退款原因不能为空");
GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId());
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
TimiException.requiredTrue(existing.getBizId().equals(order.getId()) && existing.getOperationType() == GaoFundRecord.OperationType.REFUND && existing.getAmount().equals(request.getAmount()), "requestNo 已被其他资金操作使用");
return existing;
}
TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.CANCELLED, "只有已取消订单可以退款");
TimiException.requiredTrue(order.getRefundStatus() == GaoSalesOrder.RefundStatus.PENDING, "当前订单不在待退款状态");
long refundableAmount = safe(order.getPaidAmount()) - safe(order.getRefundedAmount());
TimiException.requiredTrue(request.getAmount() <= refundableAmount, "退款金额不能超过有效收款余额");
TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "退款必须上传照片");
GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.REFUND, requestNo, request.getAmount(), request.getOccurredAt(), request.getRemark());
record.setStatus(GaoFundRecord.Status.VERIFIED);
record.setReason(reason);
fundRecordService.createByOrderPayment(record);
long refundedAmount = safe(order.getRefundedAmount()) + request.getAmount();
order.setRefundedAmount(refundedAmount);
order.setRefundStatus(refundedAmount == safe(order.getPaidAmount())
? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING);
orderService.updatePayment(order);
attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList());
return record;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord confirmNoRefund(GaoOrderPaymentNoRefundRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CONFIRM_NO_REFUND", request, () -> {
TimiException.required(request, "not found no refund request");
GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId());
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
String reason = requiredText(request.getReason(), "确认无需退款原因不能为空");
GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
return existing;
}
TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.CANCELLED
&& order.getRefundStatus() == GaoSalesOrder.RefundStatus.PENDING
&& safe(order.getRefundedAmount()) == 0, "当前订单不在待确认退款状态");
GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.CONFIRM_NO_REFUND, requestNo, 0L, request.getOccurredAt(), request.getRemark());
record.setStatus(GaoFundRecord.Status.VERIFIED);
record.setReason(reason);
fundRecordService.createByOrderPayment(record);
order.setRefundStatus(GaoSalesOrder.RefundStatus.REFUNDED);
orderService.updatePayment(order);
return record;
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord revoke(GaoOrderPaymentRevokeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_FUND_REVOKE", request, () -> revokeInternal(request));
}
private GaoFundRecord revokeInternal(GaoOrderPaymentRevokeRequest request) {
TimiException.required(request, "not found fund revoke request");
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
String reason = requiredText(request.getReason(), "资金冲正原因不能为空");
GaoFundRecord source = lockOrderRecord(request.getFundRecordId());
GaoSalesOrder order = orderService.getForUpdate(source.getBizId());
GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用");
return existing;
}
TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正");
TimiException.requiredNull(fundRecordService.getRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正");
if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) {
order.setPaidAmount(safe(order.getPaidAmount()) - source.getAmount());
orderService.recomputePaymentStatus(order);
} else if (source.getOperationType() == GaoFundRecord.OperationType.REFUND) {
order.setRefundedAmount(safe(order.getRefundedAmount()) - source.getAmount());
order.setRefundStatus(GaoSalesOrder.RefundStatus.PENDING);
} else {
order.setRefundStatus(GaoSalesOrder.RefundStatus.PENDING);
}
GaoFundRecord revoke = newRecord(order, GaoFundRecord.OperationType.REVOKE, requestNo, source.getAmount(), request.getOccurredAt(), request.getRemark());
revoke.setStatus(GaoFundRecord.Status.VERIFIED);
revoke.setSourceRecordId(source.getId());
revoke.setReason(reason);
fundRecordService.createByOrderPayment(revoke);
orderService.updatePayment(order);
TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "资金冲正必须上传照片");
attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList());
return revoke;
}
private GaoFundRecord lockOrderRecord(String id) {
GaoFundRecord record = fundRecordService.getForUpdate(id);
TimiException.required(record, "not found fund record");
TimiException.requiredTrue(record.getBizType() == GaoFundRecord.BizType.SALES_ORDER, "资金记录不是销售订单资金");
return record;
}
private GaoFundRecord newRecord(GaoSalesOrder order, GaoFundRecord.OperationType operationType, String requestNo, Long amount, Long occurredAt, String remark) {
GaoFundRecord record = new GaoFundRecord();
record.setStoreId(order.getStoreId());
record.setCustomerId(order.getCustomerId());
record.setBizType(GaoFundRecord.BizType.SALES_ORDER);
record.setBizId(order.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 boolean hasPhoto(List<Attachment> attachmentList) {
for (Attachment attachment : TimiJava.safeIterable(attachmentList)) {
if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) {
return true;
}
}
return false;
}
private void verifySameReceive(GaoFundRecord existing, GaoSalesOrder order, GaoOrderPaymentReceiveRequest request) {
TimiException.requiredTrue(existing.getBizType() == GaoFundRecord.BizType.SALES_ORDER
&& existing.getBizId().equals(order.getId())
&& existing.getOperationType() == GaoFundRecord.OperationType.RECEIVE
&& existing.getAmount().equals(request.getAmount()), "requestNo 已被其他资金操作使用");
}
private String requiredText(String value, String message) {
TimiException.required(value, message);
String result = value.trim();
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
private long safe(Long value) {
return value == null ? 0L : value;
}
}
@@ -3,13 +3,17 @@ package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper;
import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventMapper;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
@@ -38,7 +42,8 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
private final GaoPackageConfigMapper mapper; private final GaoPackageConfigMapper mapper;
private final GaoPackageConfigItemMapper itemMapper; private final GaoPackageConfigItemMapper itemMapper;
private final GaoEventMapper eventMapper; private final GaoProductService productService;
private final GaoProductUnitService unitService;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final LoggerService loggerService; private final LoggerService loggerService;
@@ -131,7 +136,7 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
dbConfig.setUpdatedBy(userLoginService.getRequireLoginUserId()); dbConfig.setUpdatedBy(userLoginService.getRequireLoginUserId());
super.update(dbConfig); super.update(dbConfig);
if (request.getItemList() != null) { if (request.getItemList() != null) {
verifyItems(request.getItemList()); verifyItems(dbConfig.getStoreId(), request.getItemList());
GaoPackageConfigItem example = new GaoPackageConfigItem(); GaoPackageConfigItem example = new GaoPackageConfigItem();
example.setPackageConfigId(dbConfig.getId()); example.setPackageConfigId(dbConfig.getId());
itemMapper.deleteAllByExample(example); itemMapper.deleteAllByExample(example);
@@ -145,7 +150,6 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_DELETE", id, () -> { loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_DELETE", id, () -> {
GaoPackageConfig config = get(id); GaoPackageConfig config = get(id);
TimiException.required(config, "not found package config"); TimiException.required(config, "not found package config");
TimiException.requiredTrue(0 == eventMapper.countByPackageConfigId(id), "套餐配置已绑定购买事件,不能删除");
super.delete(id); super.delete(id);
return id; return id;
}); });
@@ -169,26 +173,35 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
config.setSort(TimiJava.defaultIfNull(config.getSort(), 0)); config.setSort(TimiJava.defaultIfNull(config.getSort(), 0));
TimiException.requiredTrue(config.getBeginAt() == null || config.getEndAt() == null || config.getBeginAt() <= config.getEndAt(), "套餐有效期无效"); TimiException.requiredTrue(config.getBeginAt() == null || config.getEndAt() == null || config.getBeginAt() <= config.getEndAt(), "套餐有效期无效");
if (config.getItemList() != null) { if (config.getItemList() != null) {
verifyItems(config.getItemList()); verifyItems(config.getStoreId(), config.getItemList());
} }
} }
private void verifyItems(List<GaoPackageConfigItem> itemList) { private void verifyItems(String storeId, List<GaoPackageConfigItem> itemList) {
TimiException.requiredTrue(!itemList.isEmpty(), "套餐至少配置一个产品"); TimiException.requiredTrue(!itemList.isEmpty(), "套餐至少配置一个产品");
TimiException.required(storeId, "not found packageConfig.storeId");
Set<String> keySet = new HashSet<>(); Set<String> keySet = new HashSet<>();
for (int i = 0; i < itemList.size(); i++) { for (int i = 0; i < itemList.size(); i++) {
GaoPackageConfigItem item = itemList.get(i); GaoPackageConfigItem item = itemList.get(i);
TimiException.required(item, "套餐产品明细不能为空"); TimiException.required(item, "套餐产品明细不能为空");
String name = trimRequired(item.getProductName(), "产品名称不能为空"); TimiException.required(item.getProductId(), "产品不能为空");
String unit = trimRequired(item.getUnit(), "产品单位不能为空"); TimiException.required(item.getProductUnitId(), "产品单位不能为空");
GaoProductUnitSelection selection = productService.getSellableProductUnit(storeId, item.getProductId(), item.getProductUnitId());
GaoProduct product = selection.getProduct();
GaoProductUnit unitEntity = selection.getUnit();
BigDecimal quantity = item.getDefaultQuantity(); BigDecimal quantity = item.getDefaultQuantity();
TimiException.required(quantity, "产品默认数量不能为空"); TimiException.required(quantity, "产品默认数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0"); TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0");
TimiException.requiredTrue(3 >= quantity.scale(), "产品数量最多保留三位小数"); TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "产品数量最多保留三位小数");
String key = name + "|" + unit; if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) {
TimiException.requiredTrue(keySet.add(key), "同一套餐内产品名称和单位不能重复"); TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
item.setProductName(name); }
item.setUnit(unit); unitService.convertToBase(unitEntity, quantity);
String key = item.getProductId() + "|" + item.getProductUnitId();
TimiException.requiredTrue(keySet.add(key), "同一套餐内产品和单位不能重复");
item.setProductNameSnapshot(product.getName());
item.setUnitSnapshot(unitEntity.getName());
item.setConversionToBaseSnapshot(unitEntity.getConversionToBase());
item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
} }
} }
@@ -197,8 +210,11 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
for (GaoPackageConfigItem source : TimiJava.safeIterable(itemList)) { for (GaoPackageConfigItem source : TimiJava.safeIterable(itemList)) {
GaoPackageConfigItem item = new GaoPackageConfigItem(); GaoPackageConfigItem item = new GaoPackageConfigItem();
item.setPackageConfigId(configId); item.setPackageConfigId(configId);
item.setProductName(source.getProductName()); item.setProductId(source.getProductId());
item.setUnit(source.getUnit()); item.setProductUnitId(source.getProductUnitId());
item.setProductNameSnapshot(source.getProductNameSnapshot());
item.setUnitSnapshot(source.getUnitSnapshot());
item.setConversionToBaseSnapshot(source.getConversionToBaseSnapshot());
item.setDefaultQuantity(source.getDefaultQuantity()); item.setDefaultQuantity(source.getDefaultQuantity());
item.setSort(source.getSort()); item.setSort(source.getSort());
item.setRemark(source.getRemark()); item.setRemark(source.getRemark());
@@ -1,431 +0,0 @@
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.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
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.RoleChecker;
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<GaoPackageDelivery, String> 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 RoleChecker roleChecker;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoPackageDelivery, String> 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<GaoPackageDelivery> pageByQuery(GaoPackageDeliveryPage page) {
TimiException.required(page, "not found page");
PageResult<GaoPackageDelivery> result = new PageResult<>();
result.setTotal(mapper.countByQuery(page));
result.setList(mapper.selectByQuery(page));
return result;
}
@Override
public List<GaoPackageDelivery> listByCustomerPackageId(String customerPackageId) {
TimiException.required(customerPackageId, "not found customerPackageId");
return mapper.selectByCustomerPackageId(customerPackageId);
}
@Override
public Map<String, List<GaoPackageDeliveryItem>> mapItemListByDeliveryIdList(Collection<String> 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, "当前套餐不可发放");
checkPaymentForDelivery(packageEntity);
List<GaoPackageDeliveryItem> requestItemList = validateRequestItems(request.getItemList());
Map<String, GaoCustomerPackageItem> 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 <= available.compareTo(requestItem.getQuantity()), "产品可发数量不足");
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, "当前套餐不可核销发放");
checkPaymentForDelivery(packageEntity);
List<GaoPackageDeliveryItem> 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<GaoPackageDeliveryItem> 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<GaoCustomerPackageItem> 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<GaoPackageDeliveryItem> validateRequestItems(List<GaoPackageDeliveryItem> sourceList) {
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "发放至少需要一个产品");
Set<String> idSet = new HashSet<>();
List<GaoPackageDeliveryItem> 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<GaoPackageDeliveryItem> existingItems = itemMapper.selectByDeliveryId(existing.getId());
List<GaoPackageDeliveryItem> requestItems = validateRequestItems(request.getItemList());
TimiException.requiredTrue(existingItems.size() == requestItems.size(), "requestNo 对应请求内容不一致");
Map<String, BigDecimal> 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<Attachment> 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 canDeliver(GaoCustomerPackage packageEntity) {
return safeLong(packageEntity.getSaleAmount()) == 0 || 0 < safeLong(packageEntity.getPaidAmount());
}
private void checkPaymentForDelivery(GaoCustomerPackage packageEntity) {
long saleAmount = safeLong(packageEntity.getSaleAmount());
long paidAmount = safeLong(packageEntity.getPaidAmount());
TimiException.requiredTrue(canDeliver(packageEntity), "套餐未有已核销收款,不能发放");
if (0 < paidAmount && paidAmount < saleAmount) {
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
}
}
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;
}
private long safeLong(Long value) {
return value == null ? 0L : value;
}
}
@@ -5,8 +5,8 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount; import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper; import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService; import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage; import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
@@ -15,13 +15,13 @@ import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
/// GAO 客户积分账户服务实现 /// GAO 客户积分账户服务实现
/// ///
@@ -32,7 +32,7 @@ import java.util.stream.Collectors;
public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPointAccount, String> implements GaoPointAccountService { public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPointAccount, String> implements GaoPointAccountService {
private final GaoPointAccountMapper mapper; private final GaoPointAccountMapper mapper;
private final GaoCustomerMapper customerMapper; private final ObjectProvider<GaoCustomerService> customerServiceProvider;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
@Override @Override
@@ -88,8 +88,7 @@ public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPo
return; return;
} }
Map<String, GaoCustomer> customerMap = customerMapper.selectByIdList(customerIdList).stream() Map<String, GaoCustomer> customerMap = customerServiceProvider.getObject().mapByIdList(customerIdList);
.collect(Collectors.toMap(GaoCustomer::getId, item -> item));
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList); Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList);
for (GaoPointAccount account : accountList) { for (GaoPointAccount account : accountList) {
GaoCustomer customer = customerMap.get(account.getCustomerId()); GaoCustomer customer = customerMap.get(account.getCustomerId());
@@ -105,6 +105,18 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
return apply(request.getStoreId(), request.getCustomerId(), GaoPointLedger.ChangeType.ADJUST, request.getPointDelta(), null, null, null, null, idempotencyKey, request.getRemark(), operatorUserId, Time.now()); return apply(request.getStoreId(), request.getCustomerId(), GaoPointLedger.ChangeType.ADJUST, request.getPointDelta(), null, null, null, null, idempotencyKey, request.getRemark(), operatorUserId, Time.now());
} }
@Override
public List<GaoPointLedger> listRuleChangeByEventRecordId(String eventRecordId) {
TimiException.required(eventRecordId, "not found eventRecordId");
return mapper.selectRuleChangeByEventRecordId(eventRecordId);
}
@Override
public List<GaoPointLedger> listByIdempotencyKeyList(List<String> idempotencyKeyList) {
TimiException.required(idempotencyKeyList, "not found idempotencyKeyList");
return mapper.selectByIdempotencyKeyList(idempotencyKeyList);
}
@Override @Override
public PageResult<GaoPointLedger> pageByQuery(GaoPointLedgerPage page) { public PageResult<GaoPointLedger> pageByQuery(GaoPointLedgerPage page) {
TimiException.required(page, "not found page"); TimiException.required(page, "not found page");
@@ -10,9 +10,8 @@ import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger; import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.entity.GaoPointRule; import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService; import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
import com.imyeyu.api.modules.gao.service.GaoPointRuleService; import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
@@ -23,6 +22,7 @@ import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -47,8 +47,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
private final GaoPointRuleService ruleService; private final GaoPointRuleService ruleService;
private final GaoPointLedgerService ledgerService; private final GaoPointLedgerService ledgerService;
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final GaoPointLedgerMapper ledgerMapper; private final ObjectProvider<GaoEventRecordService> eventRecordServiceProvider;
private final GaoEventRecordMapper eventRecordMapper;
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
@@ -81,20 +80,20 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
} }
} }
record.setEarnedPointValue(pointDeltaTotal); record.setEarnedPointValue(pointDeltaTotal);
eventRecordMapper.updateEarnedPointValue(record.getId(), pointDeltaTotal); eventRecordServiceProvider.getObject().updateEarnedPointValue(record.getId(), pointDeltaTotal);
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void revokeByEventRecord(GaoEventRecord record) { public void revokeByEventRecord(GaoEventRecord record) {
TimiException.required(record, "not found event record"); TimiException.required(record, "not found event record");
List<GaoPointLedger> originLedgerList = ledgerMapper.selectRuleChangeByEventRecordId(record.getId()); List<GaoPointLedger> originLedgerList = ledgerService.listRuleChangeByEventRecordId(record.getId());
if (originLedgerList.isEmpty()) { if (originLedgerList.isEmpty()) {
return; return;
} }
Set<String> revokedKeySet = new HashSet<>(); Set<String> revokedKeySet = new HashSet<>();
List<String> revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList(); List<String> revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList();
for (GaoPointLedger ledger : ledgerMapper.selectByIdempotencyKeyList(revokeKeyList)) { for (GaoPointLedger ledger : ledgerService.listByIdempotencyKeyList(revokeKeyList)) {
revokedKeySet.add(ledger.getIdempotencyKey()); revokedKeySet.add(ledger.getIdempotencyKey());
} }
for (GaoPointLedger originLedger : originLedgerList) { for (GaoPointLedger originLedger : originLedgerList) {
@@ -162,7 +161,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
LocalDate currentDate = Instant.ofEpochMilli(record.getRegisteredAt()).atZone(zone).toLocalDate(); LocalDate currentDate = Instant.ofEpochMilli(record.getRegisteredAt()).atZone(zone).toLocalDate();
long windowBeginAt = currentDate.minusYears(amount).atStartOfDay(zone).toInstant().toEpochMilli(); long windowBeginAt = currentDate.minusYears(amount).atStartOfDay(zone).toInstant().toEpochMilli();
long windowEndAt = Math.addExact(record.getRegisteredAt(), 1L); long windowEndAt = Math.addExact(record.getRegisteredAt(), 1L);
long sumValue = TimiJava.defaultIfNull(eventRecordMapper.sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt), 0L); long sumValue = eventRecordServiceProvider.getObject().sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt);
JsonNode matchedTier = null; JsonNode matchedTier = null;
for (JsonNode tier : condition.get("tiers")) { for (JsonNode tier : condition.get("tiers")) {
@@ -196,7 +195,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
} }
private long count(GaoEventRecord record) { private long count(GaoEventRecord record) {
return TimiJava.defaultIfNull(eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null), 0L); return eventRecordServiceProvider.getObject().count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null);
} }
private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) { private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) {
@@ -0,0 +1,152 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.api.modules.gao.mapper.GaoProductCategoryMapper;
import com.imyeyu.api.modules.gao.service.GaoProductCategoryService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage;
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.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.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/// GAO 产品分类服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
@RequiredArgsConstructor
public class GaoProductCategoryServiceImplement extends AbstractEntityService<GaoProductCategory, String> implements GaoProductCategoryService {
private final GaoProductCategoryMapper mapper;
private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoProductCategory, String> mapper() {
return mapper;
}
@Override
public PageResult<GaoProductCategory> pageByQuery(GaoProductCategoryPage page) {
TimiException.required(page, "not found category page");
PageResult<GaoProductCategory> result = new PageResult<>();
result.setTotal(mapper.countByQuery(page));
result.setList(mapper.selectByQuery(page));
return result;
}
@Override
public List<GaoProductCategory> listByStore(String storeId) {
TimiException.required(storeId, "not found storeId");
return mapper.selectByStoreId(storeId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void sort(List<String> idList) {
if (TimiJava.isEmpty(idList)) {
return;
}
List<String> distinctIdList = new ArrayList<>(new LinkedHashSet<>(idList));
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_SORT", distinctIdList, () -> {
Map<String, GaoProductCategory> categoryMap = mapper.selectByIdList(new LinkedHashSet<>(distinctIdList)).stream()
.collect(Collectors.toMap(GaoProductCategory::getId, Function.identity()));
TimiException.requiredTrue(categoryMap.size() == distinctIdList.size(), "产品分类列表包含不存在的分类");
for (GaoProductCategory category : categoryMap.values()) {
storeScopeService.checkStoreId(category.getStoreId());
}
mapper.updateSort(distinctIdList, Time.now());
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoProductCategory category) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_CREATE", category, () -> {
prepare(category, null);
super.create(category);
return category;
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoProductCategory request) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_UPDATE", request, () -> {
TimiException.required(request, "not found category");
TimiException.required(request.getId(), "not found category.id");
GaoProductCategory category = get(request.getId());
TimiException.required(category, "not found category");
storeScopeService.checkStoreId(category.getStoreId());
category.setName(request.getName());
category.setSort(request.getSort());
category.setStatus(request.getStatus());
prepare(category, category.getId());
super.update(category);
return category;
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void disable(String id) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_DISABLE", id, () -> {
GaoProductCategory category = get(id);
TimiException.required(category, "not found category");
storeScopeService.checkStoreId(category.getStoreId());
category.setStatus(GaoProductCategory.Status.INACTIVE);
category.setUpdatedAt(Time.now());
category.setSort(TimiJava.defaultIfNull(category.getSort(), 0));
super.update(category);
return category;
});
}
@Override
public void delete(String id) {
disable(id);
}
@Override
public void destroy(String id) {
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "产品分类不允许物理删除");
}
private void prepare(GaoProductCategory category, String excludeId) {
TimiException.required(category, "not found category");
TimiException.required(category.getStoreId(), "not found category.storeId");
storeScopeService.checkStoreId(category.getStoreId());
String name = requiredText(category.getName(), "分类名称不能为空");
TimiException.requiredNull(mapper.selectByStoreIdAndName(category.getStoreId(), name, excludeId), "门店内分类名称不能重复");
category.setName(name);
category.setSort(TimiJava.defaultIfNull(category.getSort(), 0));
category.setStatus(TimiJava.defaultIfNull(category.getStatus(), GaoProductCategory.Status.ACTIVE));
TimiException.requiredTrue(category.getStatus() != null, "分类状态不能为空");
TimiException.required(userLoginService.getRequireLoginUserId(), "not found operatorUserId");
}
private String requiredText(String value, String message) {
TimiException.required(value, message);
String result = value.trim();
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
}
@@ -0,0 +1,284 @@
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.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.gao.mapper.GaoProductMapper;
import com.imyeyu.api.modules.gao.mapper.GaoProductUnitMapper;
import com.imyeyu.api.modules.gao.service.GaoProductCategoryService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
import com.imyeyu.api.modules.gao.vo.GaoProductPage;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection;
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.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/// GAO 产品服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
@RequiredArgsConstructor
public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct, String> implements GaoProductService {
private final GaoProductMapper mapper;
private final GaoProductUnitMapper unitMapper;
private final AttachmentService attachmentService;
private final ObjectProvider<GaoProductCategoryService> categoryServiceProvider;
private final GaoProductUnitService unitService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoProduct, String> mapper() {
return mapper;
}
@Override
public PageResult<GaoProduct> pageByQuery(GaoProductPage page) {
TimiException.required(page, "not found product page");
PageResult<GaoProduct> result = new PageResult<>();
result.setTotal(mapper.countByQuery(page));
List<GaoProduct> list = mapper.selectByQuery(page);
fillTransient(list);
result.setList(list);
return result;
}
@Override
public GaoProduct detail(String id) {
GaoProduct product = get(id);
TimiException.required(product, "not found product");
fillTransient(List.of(product));
return product;
}
@Override
public List<GaoProduct> listSellable(GaoProductPage page) {
TimiException.required(page, "not found product page");
page.setStatus(GaoProduct.Status.ACTIVE);
List<GaoProduct> list = mapper.selectByQuery(page);
fillTransient(list);
for (GaoProduct product : list) {
product.setUnitList(product.getUnitList().stream().filter(unit -> Boolean.TRUE.equals(unit.getAllowSale())).toList());
}
return list.stream().filter(product -> !product.getUnitList().isEmpty()).toList();
}
@Override
public GaoProductUnitSelection getSellableProductUnit(String storeId, String productId, String productUnitId) {
return getProductUnit(storeId, productId, productUnitId, false);
}
@Override
public GaoProductUnitSelection getProductUnit(String storeId, String productId, String productUnitId, boolean purchase) {
TimiException.required(storeId, "not found storeId");
TimiException.required(productId, "not found productId");
TimiException.required(productUnitId, "not found productUnitId");
GaoProduct product = get(productId);
TimiException.required(product, "not found product");
TimiException.requiredTrue(storeId.equals(product.getStoreId()), "产品不属于当前门店");
TimiException.requiredTrue(product.getStatus() == GaoProduct.Status.ACTIVE, "产品已停用");
GaoProductUnit unit = unitMapper.selectForUpdate(productUnitId);
TimiException.required(unit, "not found product unit");
TimiException.requiredTrue(productId.equals(unit.getProductId()), "产品单位不属于当前产品");
TimiException.requiredTrue(!purchase || Boolean.TRUE.equals(unit.getAllowPurchase()), "产品单位不允许采购入库");
TimiException.requiredTrue(purchase || Boolean.TRUE.equals(unit.getAllowSale()), "产品单位不允许销售");
return new GaoProductUnitSelection(product, unit);
}
@Override
public List<GaoProductUnit> listUnitByProductId(String productId) {
TimiException.required(productId, "not found productId");
return unitMapper.selectByProductId(productId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoProduct product) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CREATE", product, () -> {
prepare(product, null);
super.create(product);
saveUnits(product.getId(), product.getUnitList(), List.of());
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList());
return product;
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoProduct request) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_UPDATE", request, () -> {
TimiException.required(request, "not found product");
TimiException.required(request.getId(), "not found product.id");
GaoProduct product = get(request.getId());
TimiException.required(product, "not found product");
product.setCategoryId(request.getCategoryId());
product.setCode(TimiJava.defaultIfNull(request.getCode(), product.getCode()));
product.setName(TimiJava.defaultIfNull(request.getName(), product.getName()));
product.setProductType(TimiJava.defaultIfNull(request.getProductType(), product.getProductType()));
product.setBaseUnit(TimiJava.defaultIfNull(request.getBaseUnit(), product.getBaseUnit()));
product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled()));
product.setAllowDecimalQuantity(TimiJava.defaultIfNull(request.getAllowDecimalQuantity(), product.getAllowDecimalQuantity()));
product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus()));
product.setSalePrice(request.getSalePrice());
product.setReferenceCost(request.getReferenceCost());
product.setMinStockQuantity(request.getMinStockQuantity());
product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort()));
product.setRemark(request.getRemark());
List<GaoProductUnit> existingUnitList = unitMapper.selectByProductId(product.getId());
product.setUnitList(request.getUnitList() == null ? existingUnitList : request.getUnitList());
prepare(product, product.getId());
super.update(product);
if (request.getUnitList() != null) {
saveUnits(product.getId(), product.getUnitList(), existingUnitList);
}
if (request.getAttachmentList() != null) {
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList());
}
return product;
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void disable(String id) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_DISABLE", id, () -> {
GaoProduct product = get(id);
TimiException.required(product, "not found product");
product.setStatus(GaoProduct.Status.INACTIVE);
product.setUpdatedBy(userLoginService.getRequireLoginUserId());
product.setUpdatedAt(Time.now());
super.update(product);
return product;
});
}
@Override
public void delete(String id) {
disable(id);
}
@Override
public void destroy(String id) {
throw new TimiException(TimiCode.ERROR, "已建立产品库的产品不允许物理删除");
}
private void prepare(GaoProduct product, String excludeId) {
TimiException.required(product, "not found product");
TimiException.required(product.getStoreId(), "not found product.storeId");
product.setCode(requiredText(product.getCode(), "产品编码不能为空"));
product.setName(requiredText(product.getName(), "产品名称不能为空"));
product.setProductType(TimiJava.defaultIfNull(product.getProductType(), GaoProduct.ProductType.GOODS));
product.setBaseUnit(requiredText(product.getBaseUnit(), "基础单位不能为空"));
product.setStockEnabled(TimiJava.defaultIfNull(product.getStockEnabled(), product.getProductType() != GaoProduct.ProductType.SERVICE));
TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存");
product.setAllowDecimalQuantity(TimiJava.defaultIfNull(product.getAllowDecimalQuantity(), false));
product.setStatus(TimiJava.defaultIfNull(product.getStatus(), GaoProduct.Status.ACTIVE));
product.setSalePrice(nonNegative(product.getSalePrice(), "建议售价不能为负数"));
product.setReferenceCost(nonNegative(product.getReferenceCost(), "参考成本不能为负数"));
if (!Boolean.TRUE.equals(product.getStockEnabled())) {
TimiException.requiredNull(product.getMinStockQuantity(), "未启用库存的产品不能填写最低库存");
}
if (product.getMinStockQuantity() != null) {
TimiException.requiredTrue(0 <= product.getMinStockQuantity().signum(), "最低库存不能为负数");
TimiException.requiredTrue(3 >= product.getMinStockQuantity().scale(), "最低库存最多保留三位小数");
}
product.setSort(TimiJava.defaultIfNull(product.getSort(), 0));
TimiException.requiredNull(mapper.selectByStoreIdAndCode(product.getStoreId(), product.getCode(), excludeId), "门店内产品编码不能重复");
if (product.getCategoryId() != null) {
GaoProductCategory category = categoryServiceProvider.getObject().get(product.getCategoryId());
TimiException.required(category, "产品分类不存在");
TimiException.requiredTrue(product.getStoreId().equals(category.getStoreId()), "产品分类不属于当前门店");
}
product.setUnitList(unitService.validateAndNormalize(product, product.getUnitList()));
product.setUpdatedBy(userLoginService.getRequireLoginUserId());
}
private void saveUnits(String productId, List<GaoProductUnit> unitList, List<GaoProductUnit> existingUnitList) {
Map<String, GaoProductUnit> existingById = existingUnitList.stream()
.collect(Collectors.toMap(GaoProductUnit::getId, item -> item));
Map<String, GaoProductUnit> existingByName = existingUnitList.stream()
.collect(Collectors.toMap(GaoProductUnit::getName, item -> item));
Set<String> retainedIdSet = new HashSet<>();
for (GaoProductUnit source : unitList) {
GaoProductUnit existing = source.getId() == null ? existingByName.get(source.getName()) : existingById.get(source.getId());
if (source.getId() != null && existing == null && !existingUnitList.isEmpty()) {
throw new TimiException(TimiCode.ERROR, "产品单位不存在或不属于当前产品");
}
GaoProductUnit unit = new GaoProductUnit();
unit.setId(existing == null ? null : existing.getId());
unit.setProductId(productId);
unit.setName(source.getName());
unit.setConversionToBase(source.getConversionToBase());
unit.setIsBase(source.getIsBase());
unit.setAllowPurchase(source.getAllowPurchase());
unit.setAllowSale(source.getAllowSale());
unit.setSort(source.getSort());
if (existing == null) {
unitMapper.insert(unit);
} else {
retainedIdSet.add(existing.getId());
TimiException.requiredTrue(1 == unitMapper.updateUnit(unit), "产品单位更新失败");
}
}
for (GaoProductUnit existing : existingUnitList) {
if (!retainedIdSet.contains(existing.getId())) {
unitMapper.delete(existing.getId());
}
}
}
private void fillTransient(List<GaoProduct> list) {
if (list == null || list.isEmpty()) {
return;
}
List<String> productIdList = list.stream().map(GaoProduct::getId).toList();
Map<String, List<GaoProductUnit>> unitMap = unitMapper.selectByProductIdList(productIdList).stream()
.collect(Collectors.groupingBy(GaoProductUnit::getProductId));
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PRODUCT, productIdList);
for (GaoProduct product : list) {
product.setUnitList(unitMap.getOrDefault(product.getId(), List.of()));
product.setAttachmentList(attachmentMap.getOrDefault(product.getId(), List.of()));
if (product.getCategoryId() != null) {
product.setCategory(categoryServiceProvider.getObject().get(product.getCategoryId()));
}
}
}
private Long nonNegative(Long value, String message) {
if (value != null) {
TimiException.requiredTrue(0 <= value, message);
}
return value;
}
private String requiredText(String value, String message) {
TimiException.required(value, message);
String result = value.trim();
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
}
@@ -0,0 +1,374 @@
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.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.gao.entity.GaoStockRecord;
import com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductStockService;
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
import com.imyeyu.api.modules.gao.service.GaoStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
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.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.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/// GAO 产品库存服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
@RequiredArgsConstructor
public class GaoProductStockServiceImplement implements GaoProductStockService {
private static final BigDecimal ZERO = BigDecimal.ZERO;
private final GaoProductStockMapper stockMapper;
private final GaoStockRecordService recordService;
private final GaoProductService productService;
private final GaoProductUnitService unitService;
private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
public PageResult<GaoProductStock> pageByQuery(GaoProductStockPage page) {
TimiException.required(page, "not found stock page");
PageResult<GaoProductStock> result = new PageResult<>();
result.setTotal(stockMapper.countByQuery(page));
List<GaoProductStock> list = stockMapper.selectByQuery(page);
for (GaoProductStock stock : list) {
fillBalance(stock);
}
result.setList(list);
return result;
}
@Override
public GaoProductStock detail(String storeId, String productId) {
TimiException.required(storeId, "not found storeId");
TimiException.required(productId, "not found productId");
storeScopeService.checkStoreId(storeId);
GaoProduct product = productService.get(productId);
TimiException.required(product, "not found product");
TimiException.requiredTrue(storeId.equals(product.getStoreId()), "产品不属于当前门店");
GaoProductStock stock = stockMapper.selectByStoreIdAndProductId(storeId, productId);
if (stock == null) {
stock = new GaoProductStock();
stock.setStoreId(storeId);
stock.setProductId(productId);
stock.setOnHandQuantity(ZERO);
stock.setReservedQuantity(ZERO);
}
stock.setProduct(product);
fillBalance(stock);
return stock;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock initialIn(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_INITIAL_IN", request, () -> change(request, GaoStockRecord.OperationType.INITIAL_IN));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock manualIn(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_IN", request, () -> change(request, GaoStockRecord.OperationType.MANUAL_IN));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock manualOut(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_OUT", request, () -> change(request, GaoStockRecord.OperationType.MANUAL_OUT));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock stocktakeGain(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_GAIN", request, () -> change(request, GaoStockRecord.OperationType.STOCKTAKE_GAIN));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock stocktakeLoss(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_LOSS", request, () -> change(request, GaoStockRecord.OperationType.STOCKTAKE_LOSS));
}
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public boolean hasSufficientForFulfillment(String storeId, List<GaoStockChangeItem> items) {
TimiException.required(storeId, "not found storeId");
if (items == null || items.isEmpty()) {
return true;
}
List<GaoStockChangeItem> sortedItems = new ArrayList<>(items);
sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getProductId));
for (GaoStockChangeItem item : sortedItems) {
GaoProductStock stock = stockMapper.selectForUpdate(storeId, item.getProductId());
if (stock == null || decimal(stock.getOnHandQuantity()).subtract(decimal(stock.getReservedQuantity())).compareTo(item.getBaseQuantity()) < 0) {
return false;
}
}
return true;
}
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public List<GaoProductStock> reserveForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId) {
return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_RESERVE, null, null);
}
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public List<GaoProductStock> releaseForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId) {
return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_RELEASE, null, null);
}
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public List<GaoProductStock> outForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId) {
return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_OUT, null, null);
}
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public List<GaoProductStock> revokeForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId) {
return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_REVOKE, null, null);
}
private GaoProductStock change(GaoProductStockChangeRequest request, GaoStockRecord.OperationType operationType) {
TimiException.required(request, "not found stock change request");
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
String storeId = storeScopeService.resolveStoreId(request.getStoreId());
TimiException.required(storeId, "not found storeId");
boolean purchase = operationType == GaoStockRecord.OperationType.INITIAL_IN
|| operationType == GaoStockRecord.OperationType.MANUAL_IN
|| operationType == GaoStockRecord.OperationType.STOCKTAKE_GAIN;
GaoProductUnitSelection selection = productService.getProductUnit(storeId, request.getProductId(), request.getProductUnitId(), purchase);
GaoProduct product = selection.getProduct();
GaoProductUnit unit = selection.getUnit();
TimiException.requiredTrue(Boolean.TRUE.equals(product.getStockEnabled()), "该产品未启用库存管理");
BigDecimal quantity = validateQuantity(product, request.getQuantity());
BigDecimal baseQuantity = unitService.convertToBase(unit, quantity);
List<GaoStockRecord> existingList = recordService.listByRequestNo(storeId, requestNo);
if (!existingList.isEmpty()) {
GaoStockRecord existing = existingList.getFirst();
verifyExisting(existing, operationType, product.getId(), unit.getId(), quantity, baseQuantity, request.getRemark(), request.getReason());
return detail(storeId, product.getId());
}
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片");
TimiException.requiredTrue(hasReason(request.getReason()) || hasReason(request.getRemark()), "库存操作必须填写原因或备注");
GaoStockChangeItem item = GaoStockChangeItem.of(product, unit, quantity, baseQuantity);
List<GaoProductStock> result = executeInternal(storeId, product.getId(), List.of(item), requestNo,
userLoginService.getRequireLoginUserId(), operationType, request.getReason(), request.getRemark(), request.getAttachmentList());
return result.getFirst();
}
private List<GaoProductStock> executeInternal(String storeId, String bizId, List<GaoStockChangeItem> sourceList, String requestNo,
String operatorUserId, GaoStockRecord.OperationType operationType, String reason, String remark) {
return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, reason, remark, null);
}
private List<GaoProductStock> executeInternal(String storeId, String bizId, List<GaoStockChangeItem> sourceList, String requestNo,
String operatorUserId, GaoStockRecord.OperationType operationType, String reason, String remark, List<Attachment> attachmentList) {
TimiException.required(storeId, "not found storeId");
TimiException.required(bizId, "not found stock bizId");
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "库存变更至少需要一个产品");
String normalizedRequestNo = requiredText(requestNo, "not found requestNo");
storeScopeService.checkStoreId(storeId);
List<GaoStockChangeItem> itemList = new ArrayList<>(sourceList);
itemList.sort(Comparator.comparing(GaoStockChangeItem::getProductId));
Set<String> productIdSet = new HashSet<>();
for (GaoStockChangeItem item : itemList) {
TimiException.required(item, "库存变更项不能为空");
TimiException.requiredTrue(productIdSet.add(item.getProductId()), "同一库存操作不能重复产品");
TimiException.required(item.getBaseQuantity(), "基础单位数量不能为空");
TimiException.requiredTrue(0 < item.getBaseQuantity().signum(), "基础单位数量必须大于 0");
}
List<GaoStockRecord> existingList = recordService.listByRequestNo(storeId, normalizedRequestNo);
if (!existingList.isEmpty()) {
verifyExistingList(existingList, itemList, operationType, bizId);
List<GaoProductStock> result = new ArrayList<>();
for (GaoStockChangeItem item : itemList) {
result.add(detail(storeId, item.getProductId()));
}
return result;
}
List<GaoProductStock> result = new ArrayList<>();
for (GaoStockChangeItem item : itemList) {
GaoProductStock stock = lockOrCreateStock(storeId, item.getProductId());
BigDecimal onHandBefore = decimal(stock.getOnHandQuantity());
BigDecimal reservedBefore = decimal(stock.getReservedQuantity());
BigDecimal onHandDelta = onHandDelta(operationType, item.getBaseQuantity());
BigDecimal reservedDelta = reservedDelta(operationType, item.getBaseQuantity());
int updated = stockMapper.updateQuantity(stock.getId(), onHandDelta, reservedDelta, operatorUserId);
TimiException.requiredTrue(updated == 1, "库存余额已变化或库存数量不足");
BigDecimal onHandAfter = onHandBefore.add(onHandDelta);
BigDecimal reservedAfter = reservedBefore.add(reservedDelta);
GaoStockRecord record = new GaoStockRecord();
record.setStoreId(storeId);
record.setProductId(item.getProductId());
record.setProductUnitId(item.getProductUnitId());
record.setProductNameSnapshot(item.getProductNameSnapshot());
record.setUnitSnapshot(item.getUnitSnapshot());
record.setConversionToBaseSnapshot(item.getConversionToBaseSnapshot());
record.setQuantity(item.getQuantity());
record.setBaseQuantity(item.getBaseQuantity());
record.setOperationType(operationType);
record.setOnHandDelta(onHandDelta);
record.setReservedDelta(reservedDelta);
record.setOnHandBefore(onHandBefore);
record.setOnHandAfter(onHandAfter);
record.setReservedBefore(reservedBefore);
record.setReservedAfter(reservedAfter);
record.setBizType(operationType.name().startsWith("ORDER_") ? GaoStockRecord.BizType.ORDER_FULFILLMENT : GaoStockRecord.BizType.PRODUCT_STOCK);
record.setBizId(bizId);
record.setSourceRecordId(item.getSourceRecordId());
record.setRequestNo(normalizedRequestNo);
record.setOperatorUserId(operatorUserId);
record.setOccurredAt(Time.now());
record.setReason(reason);
record.setRemark(remark);
recordService.createOperationRecord(record, attachmentList);
stock.setOnHandQuantity(onHandAfter);
stock.setReservedQuantity(reservedAfter);
stock.setUpdatedBy(operatorUserId);
fillBalance(stock);
result.add(stock);
}
return result;
}
private GaoProductStock lockOrCreateStock(String storeId, String productId) {
GaoProductStock stock = stockMapper.selectForUpdate(storeId, productId);
if (stock != null) {
return stock;
}
stock = new GaoProductStock();
stock.setId(UUID.randomUUID().toString());
stock.setStoreId(storeId);
stock.setProductId(productId);
stock.setOnHandQuantity(ZERO);
stock.setReservedQuantity(ZERO);
stock.setUpdatedBy(userLoginService.getRequireLoginUserId());
stock.setCreatedAt(Time.now());
stockMapper.insertIgnore(stock);
stock = stockMapper.selectForUpdate(storeId, productId);
TimiException.required(stock, "库存余额创建失败");
return stock;
}
private BigDecimal onHandDelta(GaoStockRecord.OperationType operationType, BigDecimal quantity) {
return switch (operationType) {
case INITIAL_IN, MANUAL_IN, STOCKTAKE_GAIN, ORDER_REVOKE -> quantity;
case MANUAL_OUT, STOCKTAKE_LOSS, ORDER_OUT -> quantity.negate();
case ORDER_RESERVE, ORDER_RELEASE -> ZERO;
};
}
private BigDecimal reservedDelta(GaoStockRecord.OperationType operationType, BigDecimal quantity) {
return switch (operationType) {
case ORDER_RESERVE -> quantity;
case ORDER_RELEASE, ORDER_OUT -> quantity.negate();
default -> ZERO;
};
}
private BigDecimal validateQuantity(GaoProduct product, BigDecimal quantity) {
TimiException.required(quantity, "数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0");
TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "数量最多保留三位小数");
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
}
return quantity;
}
private void verifyExisting(GaoStockRecord existing, GaoStockRecord.OperationType operationType, String productId, String unitId,
BigDecimal quantity, BigDecimal baseQuantity, String remark, String reason) {
TimiException.requiredTrue(existing.getOperationType() == operationType
&& productId.equals(existing.getProductId())
&& unitId.equals(existing.getProductUnitId())
&& quantity.compareTo(existing.getQuantity()) == 0
&& baseQuantity.compareTo(existing.getBaseQuantity()) == 0
&& sameText(existing.getRemark(), remark)
&& sameText(existing.getReason(), reason), "requestNo 对应库存操作内容不一致");
}
private void verifyExistingList(List<GaoStockRecord> existingList, List<GaoStockChangeItem> itemList, GaoStockRecord.OperationType operationType, String bizId) {
Map<String, GaoStockRecord> recordMap = new HashMap<>();
for (GaoStockRecord record : existingList) {
TimiException.requiredTrue(record.getOperationType() == operationType && bizId.equals(record.getBizId()), "requestNo 已被其他库存操作使用");
recordMap.put(record.getProductId(), record);
}
TimiException.requiredTrue(recordMap.size() == itemList.size(), "requestNo 对应库存操作内容不一致");
for (GaoStockChangeItem item : itemList) {
GaoStockRecord record = recordMap.get(item.getProductId());
TimiException.required(record, "requestNo 对应库存操作内容不一致");
TimiException.requiredTrue(item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0, "requestNo 对应库存操作内容不一致");
}
}
private void fillBalance(GaoProductStock stock) {
stock.setOnHandQuantity(decimal(stock.getOnHandQuantity()));
stock.setReservedQuantity(decimal(stock.getReservedQuantity()));
stock.setAvailableQuantity(stock.getOnHandQuantity().subtract(stock.getReservedQuantity()));
if (stock.getProduct() == null && stock.getProductId() != null) {
stock.setProduct(productService.get(stock.getProductId()));
}
}
private boolean hasAttachment(List<Attachment> list) {
for (Attachment attachment : TimiJava.safeIterable(list)) {
if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) {
return true;
}
}
return false;
}
private boolean hasReason(String value) {
return value != null && !value.trim().isEmpty();
}
private String requiredText(String value, String message) {
TimiException.required(value, message);
String result = value.trim();
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
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;
}
}
@@ -0,0 +1,97 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/// GAO 产品单位服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
public class GaoProductUnitServiceImplement implements GaoProductUnitService {
private static final BigDecimal ONE = BigDecimal.ONE;
@Override
public List<GaoProductUnit> validateAndNormalize(GaoProduct product, List<GaoProductUnit> sourceList) {
TimiException.required(product, "not found product");
String baseUnit = requiredText(product.getBaseUnit(), "基础单位不能为空");
product.setBaseUnit(baseUnit);
List<GaoProductUnit> inputList = sourceList == null || sourceList.isEmpty()
? List.of(createBaseUnit(product)) : sourceList;
List<GaoProductUnit> result = new ArrayList<>();
Set<String> nameSet = new HashSet<>();
Set<String> idSet = new HashSet<>();
GaoProductUnit base = null;
for (int i = 0; i < inputList.size(); i++) {
GaoProductUnit source = inputList.get(i);
TimiException.required(source, "产品单位不能为空");
GaoProductUnit unit = new GaoProductUnit();
unit.setId(source.getId());
if (unit.getId() != null) {
TimiException.requiredTrue(idSet.add(unit.getId()), "产品单位 ID 不能重复");
}
unit.setName(requiredText(source.getName(), "产品单位名称不能为空"));
TimiException.requiredTrue(nameSet.add(unit.getName()), "同一产品的单位名称不能重复");
unit.setConversionToBase(TimiJava.defaultIfNull(source.getConversionToBase(), ONE));
TimiException.requiredTrue(0 < unit.getConversionToBase().signum(), "单位折算率必须大于 0");
TimiException.requiredTrue(6 >= unit.getConversionToBase().scale(), "单位折算率最多保留六位小数");
unit.setIsBase(Boolean.TRUE.equals(source.getIsBase()));
unit.setAllowPurchase(TimiJava.defaultIfNull(source.getAllowPurchase(), true));
unit.setAllowSale(TimiJava.defaultIfNull(source.getAllowSale(), true));
unit.setSort(TimiJava.defaultIfNull(source.getSort(), i));
if (unit.getIsBase()) {
TimiException.requiredNull(base, "一个产品只能有一个基础单位");
TimiException.requiredTrue(unit.getConversionToBase().compareTo(ONE) == 0, "基础单位折算率必须为 1");
TimiException.requiredTrue(baseUnit.equals(unit.getName()), "基础单位名称必须与产品基础单位一致");
base = unit;
}
result.add(unit);
}
TimiException.required(base, "产品必须有一个基础单位");
TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !Boolean.TRUE.equals(product.getStockEnabled()), "SERVICE 产品不能启用库存");
return result;
}
@Override
public BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity) {
TimiException.required(unit, "not found product unit");
TimiException.required(quantity, "数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0");
BigDecimal result = quantity.multiply(unit.getConversionToBase());
try {
return result.setScale(3, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "业务数量折算后不能精确保存为三位小数", e);
}
}
private GaoProductUnit createBaseUnit(GaoProduct product) {
GaoProductUnit unit = new GaoProductUnit();
unit.setName(product.getBaseUnit());
unit.setConversionToBase(ONE);
unit.setIsBase(true);
unit.setAllowPurchase(Boolean.TRUE.equals(product.getStockEnabled()));
unit.setAllowSale(true);
unit.setSort(0);
return unit;
}
private String requiredText(String value, String message) {
TimiException.required(value, message);
String result = value.trim();
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
}
@@ -0,0 +1,470 @@
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.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderItemMapper;
import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderMapper;
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.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
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.RoleChecker;
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 org.springframework.beans.factory.ObjectProvider;
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.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/// GAO 销售订单服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
@RequiredArgsConstructor
public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSalesOrder, String> implements GaoSalesOrderService {
private static final BigDecimal ZERO = BigDecimal.ZERO;
private final GaoSalesOrderMapper mapper;
private final GaoSalesOrderItemMapper itemMapper;
private final GaoFundRecordService fundRecordService;
private final ObjectProvider<GaoOrderFulfillmentService> fulfillmentServiceProvider;
private final ObjectProvider<GaoCustomerPackageService> customerPackageServiceProvider;
private final GaoCustomerService customerService;
private final GaoProductService productService;
private final GaoProductUnitService unitService;
private final GaoStoreService storeService;
private final GaoStoreScopeService storeScopeService;
private final GaoEventRecordService eventRecordService;
private final PermissionChecker permissionChecker;
private final RoleChecker roleChecker;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoSalesOrder, String> mapper() {
return mapper;
}
@Override
public PageResult<GaoSalesOrder> pageByQuery(GaoSalesOrderPage page) {
TimiException.required(page, "not found sales order page");
PageResult<GaoSalesOrder> result = new PageResult<>();
result.setTotal(mapper.countByQuery(page));
List<GaoSalesOrder> list = mapper.selectByQuery(page);
fillItemList(list);
result.setList(list);
return result;
}
@Override
public GaoSalesOrder detail(String id) {
GaoSalesOrder order = get(id);
TimiException.required(order, "not found sales order");
storeScopeService.checkStoreId(order.getStoreId());
order.setItemList(itemMapper.selectBySalesOrderId(order.getId()));
order.setFundRecordList(fundRecordService.listByBizId(order.getId()));
if (order.getSourceEventRecordId() != null) {
order.setSourceEventRecord(eventRecordService.get(order.getSourceEventRecordId()));
}
order.setUnpaidAmount(safe(order.getSaleAmount()) - safe(order.getPaidAmount()));
return order;
}
@Override
public GaoSalesOrder getForUpdate(String id) {
GaoSalesOrder order = mapper.selectForUpdate(id);
TimiException.required(order, "not found sales order");
storeScopeService.checkStoreId(order.getStoreId());
return order;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE", request, () -> createDirectInternal(request));
}
private GaoSalesOrder createDirectInternal(GaoDirectProductOrderCreateRequest request) {
TimiException.required(request, "not found direct product order request");
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
String storeId = storeScopeService.resolveStoreId(request.getStoreId());
TimiException.required(storeId, "not found storeId");
GaoSalesOrder existing = mapper.selectByRequestNo(storeId, requestNo);
if (existing != null) {
TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.DIRECT_PRODUCT, "requestNo 已被其他订单使用");
verifyExistingDirectItems(existing, request.getItemList());
return detail(existing.getId());
}
GaoCustomer customer = customerService.get(request.getCustomerId());
TimiException.required(customer, "not found customer");
TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店");
GaoStore store = storeService.get(storeId);
TimiException.required(store, "not found store");
storeScopeService.checkStore(store);
GaoEvent consumeEvent = storeService.validateDirectProductSaleEvent(store);
List<GaoSalesOrderItem> itemList = resolveDirectItems(storeId, request.getItemList());
long saleAmount = 0L;
for (GaoSalesOrderItem item : itemList) {
saleAmount = Math.addExact(saleAmount, item.getLineAmount());
}
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
String operatorUserId = userLoginService.getRequireLoginUserId();
GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.DIRECT_PRODUCT, requestNo, purchasedAt, operatorUserId, request.getRemark());
order.setSaleAmount(saleAmount);
order.setPaymentStatus(saleAmount == 0 ? GaoSalesOrder.PaymentStatus.PAID : GaoSalesOrder.PaymentStatus.UNPAID);
super.create(order);
for (GaoSalesOrderItem item : itemList) {
item.setSalesOrderId(order.getId());
itemMapper.insert(item);
}
GaoEventRecord record = new GaoEventRecord();
record.setCustomerId(customer.getId());
record.setStoreId(storeId);
record.setEventId(consumeEvent.getId());
record.setNumberValue(saleAmount);
record.setRegisteredAt(purchasedAt);
record.setRemark(request.getRemark());
record.setAttachmentList(request.getAttachmentList());
GaoEventRecord createdRecord = eventRecordService.createOrderConsumeRecord(record);
TimiException.requiredTrue(1 == mapper.updateSourceEventRecordId(order.getId(), createdRecord.getId()), "订单来源事件写入失败");
order.setSourceEventRecordId(createdRecord.getId());
order.setItemList(itemList);
return order;
}
@Override
public GaoSalesOrder getByStoreIdAndRequestNo(String storeId, String requestNo) {
TimiException.required(storeId, "not found storeId");
TimiException.required(requestNo, "not found requestNo");
return mapper.selectByRequestNo(storeId, requestNo);
}
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public GaoSalesOrder createPackageOrder(GaoPackageOrderCreateData data) {
TimiException.required(data, "not found package order data");
String requestNo = requiredText(data.getRequestNo(), "not found requestNo");
String storeId = requiredText(data.getStoreId(), "not found storeId");
storeScopeService.checkStoreId(storeId);
GaoSalesOrder existing = mapper.selectByRequestNo(storeId, requestNo);
if (existing != null) {
TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
&& data.getCustomerId().equals(existing.getCustomerId())
&& data.getSaleAmount().equals(existing.getSaleAmount())
&& data.getSourceEventRecordId().equals(existing.getSourceEventRecordId()), "requestNo 已被其他订单使用");
return existing;
}
GaoCustomer customer = customerService.get(data.getCustomerId());
TimiException.required(customer, "not found customer");
TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店");
TimiException.required(data.getPackageConfigId(), "not found packageConfigId");
TimiException.required(data.getSourceEventRecordId(), "not found sourceEventRecordId");
TimiException.requiredTrue(0 <= data.getSaleAmount(), "成交价不能为负数");
GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.PACKAGE, requestNo,
TimiJava.defaultIfNull(data.getPurchasedAt(), Time.now()), data.getOperatorUserId(), data.getRemark());
order.setSaleAmount(data.getSaleAmount());
order.setSourceEventRecordId(data.getSourceEventRecordId());
order.setPaymentStatus(data.getSaleAmount() == 0 ? GaoSalesOrder.PaymentStatus.PAID : GaoSalesOrder.PaymentStatus.UNPAID);
super.create(order);
return order;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoSalesOrder cancel(GaoSalesOrderCancelRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CANCEL", request, () -> cancelInternal(request));
}
private GaoSalesOrder cancelInternal(GaoSalesOrderCancelRequest request) {
TimiException.required(request, "not found cancel request");
TimiException.required(request.getSalesOrderId(), "not found salesOrderId");
TimiException.required(request.getReason(), "取消原因不能为空");
String reason = request.getReason().trim();
TimiException.requiredTrue(!reason.isEmpty(), "取消原因不能为空");
GaoSalesOrder order = getForUpdate(request.getSalesOrderId());
if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) {
return order;
}
TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.ACTIVE, "当前订单不可取消");
TimiException.requiredTrue(fundRecordService.countPendingReceiveByBizId(order.getId()) == 0, "订单存在待核销收款,不能取消");
TimiException.requiredTrue(fulfillmentServiceProvider.getObject().countPendingBySalesOrderId(order.getId()) == 0, "订单存在待履约操作,不能取消");
GaoEventRecord sourceRecord = order.getSourceEventRecordId() == null ? null : eventRecordService.get(order.getSourceEventRecordId());
if (sourceRecord != null) {
eventRecordService.revokeOrderConsumeRecord(sourceRecord);
}
String userId = userLoginService.getRequireLoginUserId();
GaoSalesOrder.RefundStatus refundStatus = safe(order.getPaidAmount()) == 0 ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING;
TimiException.requiredTrue(1 == mapper.cancelIf(order.getId(), GaoSalesOrder.Status.ACTIVE, GaoSalesOrder.Status.CANCELLED,
Time.now(), userId, reason, refundStatus), "订单状态已变化");
order.setStatus(GaoSalesOrder.Status.CANCELLED);
order.setCancelledAt(Time.now());
order.setCancelOperatorUserId(userId);
order.setCancelReason(reason);
order.setRefundStatus(refundStatus);
if (order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE) {
GaoCustomerPackageService customerPackageService = customerPackageServiceProvider.getObject();
GaoCustomerPackage packageEntity = customerPackageService.getBySalesOrderId(order.getId());
if (packageEntity != null && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) {
packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED);
packageEntity.setCancelledAt(order.getCancelledAt());
packageEntity.setCancelOperatorUserId(userId);
packageEntity.setCancelReason(reason);
customerPackageService.updateAggregateState(packageEntity);
}
}
return order;
}
@Override
public void updatePayment(GaoSalesOrder order) {
TimiException.required(order, "not found sales order");
TimiException.requiredTrue(1 == mapper.updatePayment(order.getId(), safeLong(order.getPendingPaidAmount()), safeLong(order.getPaidAmount()),
safeLong(order.getRefundedAmount()), order.getPaymentStatus(), order.getRefundStatus()), "订单付款汇总更新失败");
}
@Override
public void recomputePaymentStatus(GaoSalesOrder order) {
long saleAmount = safeLong(order.getSaleAmount());
long paidAmount = safeLong(order.getPaidAmount());
long pendingAmount = safeLong(order.getPendingPaidAmount());
TimiException.requiredTrue(0 <= paidAmount && paidAmount <= saleAmount, "有效收款超出订单金额");
TimiException.requiredTrue(0 <= pendingAmount && pendingAmount + paidAmount <= saleAmount, "待核销收款超出订单金额");
order.setPaymentStatus(saleAmount == 0 || paidAmount == saleAmount
? GaoSalesOrder.PaymentStatus.PAID : (0 < paidAmount ? GaoSalesOrder.PaymentStatus.PARTIALLY_PAID : GaoSalesOrder.PaymentStatus.UNPAID));
}
@Override
public void checkFulfillmentPermission(GaoSalesOrder order) {
TimiException.required(order, "not found sales order");
if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) {
return;
}
TimiException.requiredTrue(0 < safe(order.getPaidAmount()), "订单未有已核销收款,不能履约");
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
}
@Override
public int updateItemReservedQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateReservedQuantity(itemId, delta);
}
@Override
public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateFulfilledQuantity(itemId, delta);
}
@Override
public GaoSalesOrderItem getItemForUpdate(String itemId) {
TimiException.required(itemId, "not found salesOrderItemId");
return itemMapper.selectForUpdate(itemId);
}
@Override
public void refreshFulfillmentStatus(GaoSalesOrder order) {
TimiException.required(order, "not found sales order");
if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) {
return;
}
List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId());
boolean fulfilled = false;
if (order.getOrderType() == GaoSalesOrder.OrderType.DIRECT_PRODUCT) {
fulfilled = !itemList.isEmpty();
for (GaoSalesOrderItem item : itemList) {
if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) {
fulfilled = false;
break;
}
}
}
updateFulfillmentStatus(order, fulfilled);
}
@Override
public void updateFulfillmentStatus(GaoSalesOrder order, boolean fulfilled) {
TimiException.required(order, "not found sales order");
if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) {
return;
}
GaoSalesOrder.Status status = fulfilled ? GaoSalesOrder.Status.FULFILLED : GaoSalesOrder.Status.ACTIVE;
if (order.getStatus() != status) {
mapper.updateStatus(order.getId(), status);
order.setStatus(status);
}
}
@Override
public List<GaoSalesOrderItem> listItemByOrderId(String orderId) {
TimiException.required(orderId, "not found salesOrderId");
return itemMapper.selectBySalesOrderId(orderId);
}
private List<GaoSalesOrderItem> resolveDirectItems(String storeId, List<GaoSalesOrderItemRequest> sourceList) {
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "直接产品订单至少需要一个产品");
Set<String> productUnitKeySet = new HashSet<>();
List<GaoSalesOrderItem> result = new ArrayList<>();
boolean canOverridePrice = permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.SALES_ORDER_PRICE_OVERRIDE.getValue());
for (int i = 0; i < sourceList.size(); i++) {
GaoSalesOrderItemRequest source = sourceList.get(i);
TimiException.required(source, "订单产品明细不能为空");
TimiException.required(source.getProductId(), "产品不能为空");
TimiException.required(source.getProductUnitId(), "产品单位不能为空");
TimiException.requiredTrue(productUnitKeySet.add(source.getProductId() + "|" + source.getProductUnitId()), "同一订单不能重复产品和单位");
GaoProductUnitSelection selection = productService.getSellableProductUnit(storeId, source.getProductId(), source.getProductUnitId());
GaoProduct product = selection.getProduct();
GaoProductUnit unit = selection.getUnit();
BigDecimal quantity = validateQuantity(product, source.getQuantity());
unitService.convertToBase(unit, quantity);
Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), product.getSalePrice());
TimiException.required(unitPrice, "产品单价不能为空");
TimiException.requiredTrue(0 <= unitPrice, "产品单价不能为负数");
if (!canOverridePrice) {
TimiException.required(product.getSalePrice(), "普通员工只能使用建议售价");
TimiException.requiredTrue(product.getSalePrice().equals(unitPrice), "普通员工只能使用建议售价");
}
long lineAmount = toCentAmount(quantity, unitPrice);
GaoSalesOrderItem item = new GaoSalesOrderItem();
item.setProductId(product.getId());
item.setProductUnitId(unit.getId());
item.setProductNameSnapshot(product.getName());
item.setUnitSnapshot(unit.getName());
item.setConversionToBaseSnapshot(unit.getConversionToBase());
item.setQuantity(quantity);
item.setReservedQuantity(ZERO);
item.setFulfilledQuantity(ZERO);
item.setUnitPrice(unitPrice);
item.setLineAmount(lineAmount);
item.setSort(TimiJava.defaultIfNull(source.getSort(), i));
item.setRemark(source.getRemark());
result.add(item);
}
result.sort(Comparator.comparing(GaoSalesOrderItem::getSort));
return result;
}
private void verifyExistingDirectItems(GaoSalesOrder existing, List<GaoSalesOrderItemRequest> requestList) {
List<GaoSalesOrderItem> existingList = itemMapper.selectBySalesOrderId(existing.getId());
TimiException.requiredTrue(requestList != null && existingList.size() == requestList.size(), "requestNo 对应订单内容不一致");
Set<String> productUnitKeySet = new HashSet<>();
for (GaoSalesOrderItemRequest request : requestList) {
TimiException.required(request, "订单产品明细不能为空");
TimiException.required(request.getProductId(), "产品不能为空");
TimiException.required(request.getProductUnitId(), "产品单位不能为空");
TimiException.required(request.getQuantity(), "购买数量不能为空");
TimiException.requiredTrue(productUnitKeySet.add(request.getProductId() + "|" + request.getProductUnitId()), "同一订单不能重复产品和单位");
GaoSalesOrderItem item = existingList.stream()
.filter(value -> value.getProductId().equals(request.getProductId()) && value.getProductUnitId().equals(request.getProductUnitId()))
.findFirst().orElse(null);
TimiException.required(item, "requestNo 对应订单内容不一致");
TimiException.requiredTrue(item.getProductUnitId().equals(request.getProductUnitId())
&& item.getQuantity().compareTo(request.getQuantity()) == 0
&& (request.getUnitPrice() == null || item.getUnitPrice().equals(request.getUnitPrice())), "requestNo 对应订单内容不一致");
}
}
private GaoSalesOrder newOrder(String storeId, String customerId, GaoSalesOrder.OrderType orderType, String requestNo, Long purchasedAt, String operatorUserId, String remark) {
GaoSalesOrder order = new GaoSalesOrder();
order.setStoreId(storeId);
order.setCustomerId(customerId);
order.setOrderNo("SO-%d-%s".formatted(purchasedAt, UUID.randomUUID().toString().replace("-", "").substring(0, 8)));
order.setOrderType(orderType);
order.setStatus(GaoSalesOrder.Status.ACTIVE);
order.setRefundStatus(GaoSalesOrder.RefundStatus.NOT_REQUIRED);
order.setSaleAmount(0L);
order.setPendingPaidAmount(0L);
order.setPaidAmount(0L);
order.setRefundedAmount(0L);
order.setRequestNo(requestNo);
order.setPurchasedAt(purchasedAt);
order.setOperatorUserId(operatorUserId == null ? userLoginService.getRequireLoginUserId() : operatorUserId);
order.setRemark(remark);
return order;
}
private void fillItemList(List<GaoSalesOrder> orderList) {
for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) {
order.setItemList(itemMapper.selectBySalesOrderId(order.getId()));
order.setUnpaidAmount(safe(order.getSaleAmount()) - safe(order.getPaidAmount()));
}
}
private BigDecimal validateQuantity(GaoProduct product, BigDecimal quantity) {
TimiException.required(quantity, "购买数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "购买数量必须大于 0");
TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "购买数量最多保留三位小数");
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
}
return quantity;
}
private long toCentAmount(BigDecimal quantity, long unitPrice) {
try {
return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact();
} catch (ArithmeticException e) {
throw new TimiException(TimiCode.ERROR, "数量乘单价后不能精确保存为分", e);
}
}
private String requiredText(String value, String message) {
TimiException.required(value, message);
String result = value.trim();
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
private long safe(Long value) {
return value == null ? 0L : value;
}
private long safeLong(Long value) {
return safe(value);
}
private BigDecimal decimal(BigDecimal value) {
return value == null ? ZERO : value;
}
}
@@ -0,0 +1,118 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoStockRecord;
import com.imyeyu.api.modules.gao.mapper.GaoStockRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoStockRecordService;
import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserService;
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 lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/// GAO 库存流水服务实现
///
/// @author Codex
/// @since 2026-09-02
@Service
@RequiredArgsConstructor
public class GaoStockRecordServiceImplement extends AbstractEntityService<GaoStockRecord, String> implements GaoStockRecordService {
private final GaoStockRecordMapper mapper;
private final AttachmentService attachmentService;
private final UserService userService;
@Override
protected BaseMapper<GaoStockRecord, String> mapper() {
return mapper;
}
@Override
public PageResult<GaoStockRecord> pageByQuery(GaoStockRecordPage page) {
TimiException.required(page, "not found stock record page");
PageResult<GaoStockRecord> result = new PageResult<>();
result.setTotal(mapper.countByQuery(page));
result.setList(mapper.selectByQuery(page));
fillTransient(result.getList());
return result;
}
@Override
public List<GaoStockRecord> listByBiz(GaoStockRecord.BizType bizType, String bizId) {
TimiException.required(bizType, "not found stock bizType");
TimiException.required(bizId, "not found stock bizId");
List<GaoStockRecord> result = mapper.selectByBiz(bizType, bizId);
fillTransient(result);
return result;
}
@Override
public List<GaoStockRecord> listByRequestNo(String storeId, String requestNo) {
TimiException.required(storeId, "not found storeId");
TimiException.required(requestNo, "not found requestNo");
return mapper.selectByRequestNo(storeId, requestNo);
}
@Override
public GaoStockRecord getByBizProduct(GaoStockRecord.BizType bizType, String bizId, String productId,
GaoStockRecord.OperationType operationType) {
TimiException.required(bizType, "not found stock bizType");
TimiException.required(bizId, "not found stock bizId");
TimiException.required(productId, "not found productId");
TimiException.required(operationType, "not found operationType");
return mapper.selectByBizProduct(bizType, bizId, productId, operationType);
}
@Override
public void createOperationRecord(GaoStockRecord record, List<Attachment> attachmentList) {
TimiException.required(record, "not found stock record");
mapper.insert(record);
if (attachmentList != null && !attachmentList.isEmpty()) {
attachmentService.updateByBizId(Attachment.BizType.GAO_STOCK_RECORD, record.getId(), attachmentList);
}
}
@Override
public void create(GaoStockRecord record) {
throw new TimiException(TimiCode.ERROR, "库存流水必须通过库存操作创建");
}
@Override
public void update(GaoStockRecord record) {
throw new TimiException(TimiCode.ERROR, "库存流水不允许修改");
}
@Override
public void delete(String id) {
throw new TimiException(TimiCode.ERROR, "库存流水不允许删除");
}
@Override
public void destroy(String id) {
throw new TimiException(TimiCode.ERROR, "库存流水不允许物理删除");
}
private void fillTransient(List<GaoStockRecord> recordList) {
if (recordList == null || recordList.isEmpty()) {
return;
}
List<String> idList = recordList.stream().map(GaoStockRecord::getId).toList();
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_STOCK_RECORD, idList);
List<String> userIdList = recordList.stream().map(GaoStockRecord::getOperatorUserId)
.filter(value -> value != null && !value.isBlank()).distinct().toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
for (GaoStockRecord record : recordList) {
record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
record.setOperator(userMap.get(record.getOperatorUserId()));
}
}
}
@@ -2,14 +2,15 @@ package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.mapper.GaoStoreBusinessDayMapper; import com.imyeyu.api.modules.gao.mapper.GaoStoreBusinessDayMapper;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -25,7 +26,7 @@ import java.util.List;
public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<GaoStoreBusinessDay, String> implements GaoStoreBusinessDayService { public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<GaoStoreBusinessDay, String> implements GaoStoreBusinessDayService {
private final GaoStoreBusinessDayMapper mapper; private final GaoStoreBusinessDayMapper mapper;
private final GaoEventRecordMapper eventRecordMapper; private final ObjectProvider<GaoEventRecordService> eventRecordServiceProvider;
@Override @Override
protected BaseMapper<GaoStoreBusinessDay, String> mapper() { protected BaseMapper<GaoStoreBusinessDay, String> mapper() {
@@ -112,7 +113,7 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
businessDayNo++; businessDayNo++;
businessDay.setBusinessDayNo(businessDayNo); businessDay.setBusinessDayNo(businessDayNo);
mapper.update(businessDay); mapper.update(businessDay);
eventRecordMapper.updateBusinessDayNoByStoreIdAndRegisteredDateValue(storeId, businessDay.getDateValue(), businessDayNo); eventRecordServiceProvider.getObject().updateBusinessDayNo(storeId, businessDay.getDateValue(), businessDayNo);
} }
} }
@@ -124,8 +125,8 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
if (mapper.selectByStoreIdAndDateValue(storeId, dateValue) != null) { if (mapper.selectByStoreIdAndDateValue(storeId, dateValue) != null) {
return; return;
} }
Long count = eventRecordMapper.countByStoreIdAndRegisteredDateValue(storeId, dateValue); long count = eventRecordServiceProvider.getObject().countByStoreIdAndRegisteredDateValue(storeId, dateValue);
if (0 < TimiJava.defaultIfNull(count, 0L)) { if (0 < count) {
return; return;
} }
GaoStoreBusinessDay businessDay = new GaoStoreBusinessDay(); GaoStoreBusinessDay businessDay = new GaoStoreBusinessDay();
@@ -158,7 +159,7 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
} }
private void checkNoRecord(GaoStoreBusinessDay businessDay) { private void checkNoRecord(GaoStoreBusinessDay businessDay) {
Long count = eventRecordMapper.countByStoreIdAndRegisteredDateValue(businessDay.getStoreId(), businessDay.getDateValue()); long count = eventRecordServiceProvider.getObject().countByStoreIdAndRegisteredDateValue(businessDay.getStoreId(), businessDay.getDateValue());
TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, "该营业日已有登记记录,不能改为休息或删除"); TimiException.requiredTrue(count < 1, "该营业日已有登记记录,不能改为休息或删除");
} }
} }
@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService; import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView; import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
@@ -25,7 +25,7 @@ import java.util.List;
public class GaoStoreChartServiceImplement implements GaoStoreChartService { public class GaoStoreChartServiceImplement implements GaoStoreChartService {
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final GaoEventRecordMapper eventRecordMapper; private final GaoEventRecordService eventRecordService;
@Override @Override
public List<GaoCustomerDailyStateView> customerDailyTrend(GaoStoreChartReq req) { public List<GaoCustomerDailyStateView> customerDailyTrend(GaoStoreChartReq req) {
@@ -46,19 +46,19 @@ public class GaoStoreChartServiceImplement implements GaoStoreChartService {
@Override @Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req) { public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req) {
checkRangeReq(req); checkRangeReq(req);
return eventRecordMapper.stateDailyByRange(req); return eventRecordService.stateDailyByRange(req);
} }
@Override @Override
public List<GaoEventRecordNumberValueDailyStatView> eventRecordNumberValueDailyStat(GaoStoreChartReq req) { public List<GaoEventRecordNumberValueDailyStatView> eventRecordNumberValueDailyStat(GaoStoreChartReq req) {
checkRangeReq(req); checkRangeReq(req);
return eventRecordMapper.stateNumberValueDailyByRange(req); return eventRecordService.stateNumberValueDailyByRange(req);
} }
@Override @Override
public List<GaoEventRecordCalendarView> eventRecordCalendar(GaoStoreChartReq req) { public List<GaoEventRecordCalendarView> eventRecordCalendar(GaoStoreChartReq req) {
checkRangeReq(req); checkRangeReq(req);
return eventRecordMapper.selectCalendarByRange(req); return eventRecordService.calendarByStoreRange(req);
} }
private void checkRangeReq(GaoStoreChartReq req) { private void checkRangeReq(GaoStoreChartReq req) {
@@ -3,7 +3,6 @@ package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Article; import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.mapper.ArticleMapper;
import com.imyeyu.api.modules.common.service.ArticleService; import com.imyeyu.api.modules.common.service.ArticleService;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.mapper.GaoStoreDiaryMapper; import com.imyeyu.api.modules.gao.mapper.GaoStoreDiaryMapper;
@@ -26,7 +25,6 @@ public class GaoStoreDiaryServiceImplement implements GaoStoreDiaryService {
private final ArticleService articleService; private final ArticleService articleService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final ArticleMapper articleMapper;
private final GaoStoreDiaryMapper diaryMapper; private final GaoStoreDiaryMapper diaryMapper;
@Override @Override
@@ -83,7 +81,7 @@ public class GaoStoreDiaryServiceImplement implements GaoStoreDiaryService {
Article example = new Article(); Article example = new Article();
example.setBizType(Article.BizType.GAO_STORE_DIARY); example.setBizType(Article.BizType.GAO_STORE_DIARY);
example.setBizId(storeId); example.setBizId(storeId);
for (Article article : articleMapper.selectAllByExample(example)) { for (Article article : articleService.listByExample(example)) {
attachmentService.deleteByBizId(Attachment.BizType.ARTICLE, article.getId()); attachmentService.deleteByBizId(Attachment.BizType.ARTICLE, article.getId());
articleService.delete(article.getId()); articleService.delete(article.getId());
} }
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Tag; import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.service.TagService; import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.mapper.GaoStoreMapper; import com.imyeyu.api.modules.gao.mapper.GaoStoreMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerService;
@@ -79,6 +80,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
try { try {
logger.setContent(jackson.writeValueAsString(store)); logger.setContent(jackson.writeValueAsString(store));
checkUnique(store); checkUnique(store);
validateDirectProductSaleEventIfPresent(store);
store.setStatus(TimiJava.defaultIfNull(store.getStatus(), GaoStore.Status.ACTIVE)); store.setStatus(TimiJava.defaultIfNull(store.getStatus(), GaoStore.Status.ACTIVE));
store.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false)); store.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
store.setSort(TimiJava.defaultIfNull(store.getSort(), 0)); store.setSort(TimiJava.defaultIfNull(store.getSort(), 0));
@@ -111,6 +113,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
try { try {
logger.setContent(jackson.writeValueAsString(store)); logger.setContent(jackson.writeValueAsString(store));
checkUnique(store); checkUnique(store);
validateDirectProductSaleEventIfPresent(store);
super.update(store); super.update(store);
logger.setResult(store.getId()); logger.setResult(store.getId());
} catch (TimiException e) { } catch (TimiException e) {
@@ -224,6 +227,56 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
return mapper.selectByUserId(userId); return mapper.selectByUserId(userId);
} }
@Override
public GaoEvent validateDirectProductSaleEvent(GaoStore store) {
TimiException.required(store, "not found store");
TimiException.required(store.getDirectProductSaleEventId(), "门店未配置直接产品消费事件");
TimiException.required(store.getId(), "门店尚未生成 ID,不能绑定直接产品消费事件");
GaoEvent event = eventService.get(store.getDirectProductSaleEventId());
TimiException.required(event, "直接产品消费事件不存在");
TimiException.requiredTrue(store.getId().equals(event.getStoreId()), "直接产品消费事件不属于当前门店");
TimiException.requiredTrue(event.getStatus() == GaoEvent.Status.ACTIVE, "直接产品消费事件未启用");
TimiException.requiredTrue(event.getBizType() == GaoEvent.BizType.GENERAL, "直接产品消费事件必须是 GENERAL 类型");
TimiException.requiredTrue(event.getValueType() == GaoEvent.ValueType.NUMBER && event.getValueUnit() == GaoEvent.ValueUnit.CENT, "直接产品消费事件必须使用 NUMBER/CENT 数值");
TimiException.requiredTrue(!Boolean.TRUE.equals(event.getRequireVerification()), "直接产品消费事件不能要求核销");
eventService.attachOptionGroupList(List.of(event));
for (var group : TimiJava.safeIterable(event.getOptionGroupList())) {
TimiException.requiredTrue(!Boolean.TRUE.equals(group.getRequired()), "直接产品消费事件不能配置必填选项");
}
return event;
}
@Override
public List<GaoEvent> listDirectProductSaleEvents(String storeId) {
TimiException.required(storeId, "not found storeId");
return eventService.listValid(storeId).stream()
.filter(this::isValidDirectProductSaleEvent)
.toList();
}
private boolean isValidDirectProductSaleEvent(GaoEvent event) {
if (event == null
|| event.getStatus() != GaoEvent.Status.ACTIVE
|| event.getBizType() != GaoEvent.BizType.GENERAL
|| event.getValueType() != GaoEvent.ValueType.NUMBER
|| event.getValueUnit() != GaoEvent.ValueUnit.CENT
|| Boolean.TRUE.equals(event.getRequireVerification())) {
return false;
}
for (var group : TimiJava.safeIterable(event.getOptionGroupList())) {
if (Boolean.TRUE.equals(group.getRequired())) {
return false;
}
}
return true;
}
private void validateDirectProductSaleEventIfPresent(GaoStore store) {
if (store.getDirectProductSaleEventId() != null && !store.getDirectProductSaleEventId().isBlank()) {
validateDirectProductSaleEvent(store);
}
}
@Override @Override
public String getBelongIdByRequiredLoginUserId() { public String getBelongIdByRequiredLoginUserId() {
return getByUserId(userLoginService.getRequireLoginUserId()).getId(); return getByUserId(userLoginService.getRequireLoginUserId()).getId();
@@ -25,15 +25,4 @@ public class GaoCustomerPackagePage extends BasePage {
/// 套餐状态 /// 套餐状态
private GaoCustomerPackage.Status status; private GaoCustomerPackage.Status status;
/// 付款状态
private GaoCustomerPackage.PaymentStatus paymentStatus;
/// 退款状态
private GaoCustomerPackage.RefundStatus refundStatus;
/// 购买开始时间
private Long beginAt;
/// 购买结束时间
private Long endAt;
} }
@@ -11,11 +11,20 @@ import java.math.BigDecimal;
@Data @Data
public class GaoCustomerPackagePurchaseItem { public class GaoCustomerPackagePurchaseItem {
/// 产品名称 /// 产品 ID
private String productName; private String productId;
/// 产品单位 /// 产品单位 ID
private String unit; private String productUnitId;
/// 产品名称快照,由后端生成
private String productNameSnapshot;
/// 单位快照,由后端生成
private String unitSnapshot;
/// 折算率快照,由后端生成
private BigDecimal conversionToBaseSnapshot;
/// 总权益数量 /// 总权益数量
private BigDecimal totalQuantity; private BigDecimal totalQuantity;
@@ -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-09-02
@Data
public class GaoDirectProductOrderCreateRequest {
/// 创建订单幂等号
private String requestNo;
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 下单时间
private Long purchasedAt;
/// 订单明细
private List<GaoSalesOrderItemRequest> itemList;
/// 购买照片
private List<Attachment> attachmentList;
/// 备注
private String remark;
}
@@ -19,6 +19,12 @@ public class GaoFundRecordPage extends BasePage {
/// 客户套餐 ID /// 客户套餐 ID
private String customerPackageId; private String customerPackageId;
/// 销售订单 ID
private String salesOrderId;
/// 资金业务类型
private GaoFundRecord.BizType bizType;
/// 客户 ID /// 客户 ID
private String customerId; private String customerId;
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// GAO 统一履约状态操作请求
///
/// @author Codex
/// @since 2026-09-02
@Data
public class GaoOrderFulfillmentActionRequest {
/// 履约单 ID
private String fulfillmentId;
/// 拒绝或撤销原因
private String reason;
}
@@ -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-09-02
@Data
public class GaoOrderFulfillmentCreateRequest {
/// 销售订单 ID
private String salesOrderId;
/// 客户端幂等请求号
private String requestNo;
/// 实际履约时间
private Long deliveredAt;
/// 履约明细
private List<GaoOrderFulfillmentItemRequest> itemList;
/// 履约照片
private List<Attachment> attachmentList;
/// 备注
private String remark;
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.math.BigDecimal;
/// GAO 统一履约明细请求
///
/// @author Codex
/// @since 2026-09-02
@Data
public class GaoOrderFulfillmentItemRequest {
/// 直接订单明细 ID,与客户套餐权益 ID 二选一
private String sourceOrderItemId;
/// 客户套餐权益项 ID,与直接订单明细 ID 二选一
private String sourceCustomerPackageItemId;
/// 本次履约数量
private BigDecimal quantity;
}
@@ -0,0 +1,36 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
import com.imyeyu.java.bean.BasePage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// GAO 统一履约分页查询请求
///
/// @author Codex
/// @since 2026-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoOrderFulfillmentPage extends BasePage {
/// 门店 ID
private String storeId;
/// 销售订单 ID
private String salesOrderId;
/// 客户 ID
private String customerId;
/// 履约类型
private GaoOrderFulfillment.FulfillmentType fulfillmentType;
/// 履约状态
private GaoOrderFulfillment.Status status;
/// 起始时间
private Long beginAt;
/// 结束时间
private Long endAt;
}
@@ -0,0 +1,26 @@
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-09-02
@Data
public class GaoOrderFulfillmentPrepareStockRequest {
/// 履约单 ID
private String fulfillmentId;
/// 占库准备原因
private String reason;
/// 备注
private String remark;
/// 补充照片
private List<Attachment> attachmentList;
}
@@ -1,31 +1,33 @@
package com.imyeyu.api.modules.gao.vo; package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
/// GAO 套餐发放请求 /// GAO 已核销履约整单冲正请求
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Data @Data
public class GaoPackageDeliveryCreateRequest { public class GaoOrderFulfillmentRevokeRequest {
/// 原履约单 ID
private String fulfillmentId;
/// 客户端幂等请求号 /// 客户端幂等请求号
private String requestNo; private String requestNo;
/// 客户套餐 ID /// 冲正发生时间
private String customerPackageId;
/// 实际发放时间
private Long deliveredAt; private Long deliveredAt;
/// 发放产品明细 /// 冲正原因
private List<GaoPackageDeliveryItem> itemList; private String reason;
/// 发放照片 /// true 为实物已退回门店
private Boolean returnedToStore;
/// 退回或冲正照片
private List<Attachment> attachmentList; private List<Attachment> attachmentList;
/// 备注 /// 备注
@@ -2,16 +2,16 @@ package com.imyeyu.api.modules.gao.vo;
import lombok.Data; import lombok.Data;
/// GAO 资金收款状态操作请求 /// GAO 销售订单收款操作请求
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Data @Data
public class GaoFundActionRequest { public class GaoOrderPaymentActionRequest {
/// 资金记录 ID /// 资金流水 ID
private String fundRecordId; private String fundRecordId;
/// 拒绝或撤销原因 /// 原因
private String reason; private String reason;
} }
@@ -2,25 +2,25 @@ package com.imyeyu.api.modules.gao.vo;
import lombok.Data; import lombok.Data;
/// GAO 确认无需退款请求 /// GAO 销售订单确认无需退款请求
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Data @Data
public class GaoFundConfirmNoRefundRequest { public class GaoOrderPaymentNoRefundRequest {
/// 客户端幂等请求号 /// 订单 ID
private String salesOrderId;
/// 幂等请求号
private String requestNo; private String requestNo;
/// 客户套餐 ID
private String customerPackageId;
/// 确认时间
private Long occurredAt;
/// 确认原因 /// 确认原因
private String reason; private String reason;
/// 发生时间
private Long occurredAt;
/// 备注 /// 备注
private String remark; private String remark;
} }
@@ -5,23 +5,23 @@ import lombok.Data;
import java.util.List; import java.util.List;
/// GAO 线下收款登记请求 /// GAO 销售订单收款请求
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Data @Data
public class GaoFundReceiveRequest { public class GaoOrderPaymentReceiveRequest {
/// 客户端幂等请求号 /// 订单 ID
private String salesOrderId;
/// 幂等请求号
private String requestNo; private String requestNo;
/// 客户套餐 ID
private String customerPackageId;
/// 收款金额,单位分 /// 收款金额,单位分
private Long amount; private Long amount;
/// 收款发生时间 /// 实际收款时间
private Long occurredAt; private Long occurredAt;
/// 收款照片 /// 收款照片
@@ -5,19 +5,19 @@ import lombok.Data;
import java.util.List; import java.util.List;
/// GAO 退款登记请求 /// GAO 销售订单退款请求
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Data @Data
public class GaoFundRefundRequest { public class GaoOrderPaymentRefundRequest {
/// 客户端幂等请求号 /// 订单 ID
private String salesOrderId;
/// 幂等请求号
private String requestNo; private String requestNo;
/// 客户套餐 ID
private String customerPackageId;
/// 退款金额,单位分 /// 退款金额,单位分
private Long amount; private Long amount;
@@ -5,25 +5,25 @@ import lombok.Data;
import java.util.List; import java.util.List;
/// GAO 已核销发放整单冲正请求 /// GAO 销售订单资金冲正请求
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-31 /// @since 2026-09-02
@Data @Data
public class GaoPackageDeliveryRevokeRequest { public class GaoOrderPaymentRevokeRequest {
/// 客户端幂等请求号 /// 原资金流水 ID
private String fundRecordId;
/// 幂等请求号
private String requestNo; private String requestNo;
/// 原发放单 ID
private String deliveryId;
/// 冲正发生时间
private Long deliveredAt;
/// 冲正原因 /// 冲正原因
private String reason; private String reason;
/// 冲正发生时间
private Long occurredAt;
/// 冲正照片 /// 冲正照片
private List<Attachment> attachmentList; private List<Attachment> attachmentList;
@@ -1,17 +0,0 @@
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;
}
@@ -1,36 +0,0 @@
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;
}
@@ -0,0 +1,44 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// GAO 套餐订单内部创建数据
///
/// @author Codex
/// @since 2026-09-02
@Data
public class GaoPackageOrderCreateData {
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 套餐配置 ID
private String packageConfigId;
/// 套餐名称快照
private String packageNameSnapshot;
/// 套餐标准价快照
private Long standardAmountSnapshot;
/// 成交价
private Long saleAmount;
/// 来源购买事件记录 ID
private String sourceEventRecordId;
/// 幂等号
private String requestNo;
/// 下单时间
private Long purchasedAt;
/// 操作人
private String operatorUserId;
/// 备注
private String remark;
}
@@ -0,0 +1,24 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.java.bean.BasePage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// GAO 产品分类分页请求
///
/// @author Codex
/// @since 2026-09-02
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoProductCategoryPage extends BasePage {
/// 门店 ID
private String storeId;
/// 分类状态
private GaoProductCategory.Status status;
/// 名称关键词
private String keyword;
}

Some files were not shown because too many files have changed in this diff Show More