diff --git a/pom.xml b/pom.xml
index 9ed94da..9bb7524 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
com.imyeyu.timiserverapi
TimiServerAPI
- 1.0.28
+ 1.0.29
jar
TimiServerAPI
imyeyu.com API
diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Article.java b/src/main/java/com/imyeyu/api/modules/common/entity/Article.java
index f7b4aec..c040df1 100644
--- a/src/main/java/com/imyeyu/api/modules/common/entity/Article.java
+++ b/src/main/java/com/imyeyu/api/modules/common/entity/Article.java
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.bean.attachment.SettingSupport;
+import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.PageIgnore;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
@@ -30,6 +31,9 @@ public class Article extends UUIDEntity implements SettingSupport {
/** 博客 */
BLOG,
+ /// GAO 门店日记
+ GAO_STORE_DIARY,
+
/// 反馈
FEEDBACK
}
@@ -93,6 +97,12 @@ public class Article extends UUIDEntity implements SettingSupport {
/** 文章状态 */
protected Status status;
+ /// 最后更新用户 ID
+ protected String updatedBy;
+
+ @Transient
+ protected User updater;
+
@Transient
protected List settingList;
diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java
index 6b45bbf..79489ed 100644
--- a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java
+++ b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java
@@ -40,6 +40,10 @@ public class Attachment extends UUIDEntity {
GAO_EVENT_RECORD,
+ GAO_PACKAGE_DELIVERY,
+
+ GAO_FUND_RECORD,
+
TEMP_FILE,
MIRROR
diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentVariantServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentVariantServiceImplement.java
index fe0bb3e..7b45bc0 100644
--- a/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentVariantServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentVariantServiceImplement.java
@@ -59,6 +59,7 @@ public class AttachmentVariantServiceImplement implements AttachmentVariantServi
private static final long THUMB_CACHE_TTL = Time.D * 7;
private static final long THUMB_LOCK_TTL = Time.M * 10;
private static final double THUMB_TARGET_SECONDS = 2;
+ private static final int VARIANT_NAME_MAX_LENGTH = 255;
private static final String THUMB_OUTPUT_FORMAT = "png";
private static final String THUMB_ALGORITHM_VERSION = "v3";
private static final String THUMB_LOCK_KEY_PREFIX = "ATTACHMENT:THUMB:LOCK:";
@@ -158,7 +159,7 @@ public class AttachmentVariantServiceImplement implements AttachmentVariantServi
thumbFile = createThumbFile(source, requestWidth, requestHeight, fitMode, isImage);
variant = new AttachmentVariant();
variant.setId(UUID.randomUUID().toString());
- variant.setName(Network.simpleURIFileName(source.getName()) + "." + THUMB_OUTPUT_FORMAT);
+ variant.setName(buildVariantName(source.getName()));
variant.setMimeType("image/" + THUMB_OUTPUT_FORMAT);
long expireAt = Time.now() + THUMB_CACHE_TTL;
variant.setSourceAttachmentId(source.getId());
@@ -257,6 +258,20 @@ public class AttachmentVariantServiceImplement implements AttachmentVariantServi
);
}
+ /// 构建不超过数据库字段长度的变体名称
+ ///
+ /// @param sourceName 源附件名称
+ /// @return 变体名称
+ private String buildVariantName(String sourceName) {
+ String suffix = "." + THUMB_OUTPUT_FORMAT;
+ String baseName = Network.simpleURIFileName(sourceName);
+ int maxBaseLength = VARIANT_NAME_MAX_LENGTH - suffix.length();
+ if (maxBaseLength < baseName.codePointCount(0, baseName.length())) {
+ baseName = baseName.substring(0, baseName.offsetByCodePoints(0, maxBaseLength));
+ }
+ return baseName + suffix;
+ }
+
private File createThumbFile(
Attachment source,
Integer requestWidth,
diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
index b66ff8a..0609c98 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
@@ -21,6 +21,14 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
STORE_DELETE("STORE:DELETE", "门店删除", false, true),
+ STORE_DIARY_CREATE("STORE_DIARY:CREATE", "门店日记创建", true, false),
+
+ STORE_DIARY_READ("STORE_DIARY:READ", "门店日记读取", true, false),
+
+ STORE_DIARY_UPDATE("STORE_DIARY:UPDATE", "门店日记修改", true, false),
+
+ STORE_DIARY_DELETE("STORE_DIARY:DELETE", "门店日记删除", true, false),
+
CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建", false, false),
CUSTOMER_READ("CUSTOMER:READ", "客户读取", false, false),
@@ -51,6 +59,40 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false),
+ PACKAGE_CONFIG_CREATE("PACKAGE_CONFIG:CREATE", "套餐配置创建", true, true),
+
+ PACKAGE_CONFIG_READ("PACKAGE_CONFIG:READ", "套餐配置读取", true, true),
+
+ PACKAGE_CONFIG_UPDATE("PACKAGE_CONFIG:UPDATE", "套餐配置修改", true, true),
+
+ PACKAGE_CONFIG_DELETE("PACKAGE_CONFIG:DELETE", "套餐配置删除", true, true),
+
+ CUSTOMER_PACKAGE_CREATE("CUSTOMER_PACKAGE:CREATE", "客户套餐创建", true, true),
+
+ CUSTOMER_PACKAGE_READ("CUSTOMER_PACKAGE:READ", "客户套餐读取", true, true),
+
+ CUSTOMER_PACKAGE_CANCEL("CUSTOMER_PACKAGE:CANCEL", "客户套餐取消", true, true),
+
+ CUSTOMER_PACKAGE_PRICE_OVERRIDE("CUSTOMER_PACKAGE:PRICE_OVERRIDE", "客户套餐成交价和权益配额调整", true, true),
+
+ PACKAGE_DELIVERY_CREATE("PACKAGE_DELIVERY:CREATE", "套餐发放创建", true, true),
+
+ PACKAGE_DELIVERY_READ("PACKAGE_DELIVERY:READ", "套餐发放读取", true, true),
+
+ PACKAGE_DELIVERY_VERIFY("PACKAGE_DELIVERY:VERIFY", "套餐发放核销", true, true),
+
+ PACKAGE_DELIVERY_REVOKE("PACKAGE_DELIVERY:REVOKE", "套餐发放冲正", true, true),
+
+ FUND_RECORD_READ("FUND_RECORD:READ", "资金流水读取", true, true),
+
+ FUND_RECEIVE_CREATE("FUND_RECEIVE:CREATE", "线下收款创建", true, true),
+
+ FUND_RECEIVE_VERIFY("FUND_RECEIVE:VERIFY", "线下收款核销", true, true),
+
+ FUND_REFUND("FUND:REFUND", "套餐退款", true, true),
+
+ FUND_REVOKE("FUND:REVOKE", "资金流水冲正", true, true),
+
POINT_ACCOUNT_READ("POINT_ACCOUNT:READ", "客户积分账户读取", true, true),
POINT_LEDGER_READ("POINT_LEDGER:READ", "客户积分流水读取", true, true),
diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java
index b3101f1..93ba36e 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java
@@ -16,7 +16,7 @@ public enum GaoRoleCode implements BuiltinRoleCode {
GLOBAL_MANAGER("门店管理", null),
/// 店长
- STORE_MANAGER("店长", GLOBAL_MANAGER.name());
+ STORE_MANAGER("店长", null);
final String defaultName;
final String parentCode;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java
new file mode 100644
index 0000000..4201756
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java
@@ -0,0 +1,265 @@
+package com.imyeyu.api.modules.gao.controller;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.common.service.AttachmentService;
+import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
+import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
+import com.imyeyu.api.modules.gao.entity.GaoCustomer;
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
+import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
+import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
+import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
+import com.imyeyu.api.modules.gao.service.GaoCustomerService;
+import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
+import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
+import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService;
+import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
+import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest;
+import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest;
+import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest;
+import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.annotation.AOPLog;
+import com.imyeyu.spring.annotation.RequestRateLimit;
+import com.imyeyu.spring.bean.PageResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/// GAO 客户套餐和收退款接口
+///
+/// @author Codex
+/// @since 2026-08-31
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/gao/customer/package")
+public class GaoCustomerPackageController {
+
+ private final GaoCustomerPackageService service;
+ private final GaoCustomerService customerService;
+ private final GaoEventRecordService eventRecordService;
+ private final GaoPackageDeliveryService deliveryService;
+ private final GaoFundRecordService fundRecordService;
+ private final AttachmentService attachmentService;
+ private final GaoStoreScopeService storeScopeService;
+ private final UserService userService;
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_READ)
+ @PostMapping("/list")
+ public PageResult list(@RequestBody GaoCustomerPackagePage page) {
+ TimiException.required(page, "not found page");
+ page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
+ PageResult result = service.pageByQuery(page);
+ if (result.getList().isEmpty()) {
+ return result;
+ }
+ fillTransient(result.getList());
+ Map customerMap = customerService.mapByIdList(result.getList().stream().map(GaoCustomerPackage::getCustomerId).toList());
+ for (GaoCustomerPackage packageEntity : result.getList()) {
+ packageEntity.setCustomer(customerMap.get(packageEntity.getCustomerId()));
+ }
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_READ)
+ @PostMapping("/detail")
+ public GaoCustomerPackage detail(@RequestParam String id) {
+ GaoCustomerPackage result = service.get(id);
+ TimiException.required(result, "not found customer package");
+ storeScopeService.checkStoreId(result.getStoreId());
+ fillTransient(List.of(result));
+ GaoCustomer customer = customerService.get(result.getCustomerId());
+ if (customer != null) {
+ customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId()));
+ }
+ result.setCustomer(customer);
+ GaoEventRecord purchaseRecord = eventRecordService.get(result.getPurchaseEventRecordId());
+ if (purchaseRecord != null) {
+ purchaseRecord.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, purchaseRecord.getId()));
+ }
+ result.setPurchaseEventRecord(purchaseRecord);
+ result.setDeliveryList(deliveryService.listByCustomerPackageId(result.getId()));
+ fillDeliveryTransient(result.getDeliveryList());
+ result.setFundRecordList(fundRecordService.listByCustomerPackageId(result.getId()));
+ fillFundTransient(result.getFundRecordList());
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_CREATE)
+ @PostMapping("/purchase")
+ public GaoCustomerPackage purchase(@RequestBody GaoCustomerPackagePurchaseRequest request) {
+ GaoCustomerPackage result = service.purchase(request);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_CANCEL)
+ @PostMapping("/cancel")
+ public GaoCustomerPackage cancel(@RequestBody GaoCustomerPackageCancelRequest request) {
+ GaoCustomerPackage result = service.cancel(request);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
+ @PostMapping("/payment/receive")
+ public GaoCustomerPackage receive(@RequestBody GaoFundReceiveRequest request) {
+ fundRecordService.receive(request);
+ return packageResponse(request.getCustomerPackageId());
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY)
+ @PostMapping("/payment/verify")
+ public GaoCustomerPackage verifyPayment(@RequestParam String id) {
+ fundRecordService.verify(id);
+ return packageAfterFundAction(id);
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY)
+ @PostMapping("/payment/reject")
+ public GaoCustomerPackage rejectPayment(@RequestBody GaoFundActionRequest request) {
+ fundRecordService.reject(request);
+ return packageAfterFundAction(request.getFundRecordId());
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
+ @PostMapping("/payment/cancel")
+ public GaoCustomerPackage cancelPayment(@RequestBody GaoFundActionRequest request) {
+ fundRecordService.cancel(request);
+ return packageAfterFundAction(request.getFundRecordId());
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_REFUND)
+ @PostMapping("/refund")
+ public GaoCustomerPackage refund(@RequestBody GaoFundRefundRequest request) {
+ fundRecordService.refund(request);
+ return packageResponse(request.getCustomerPackageId());
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_REFUND)
+ @PostMapping("/refund/confirm-none")
+ public GaoCustomerPackage confirmNoRefund(@RequestBody GaoFundConfirmNoRefundRequest request) {
+ fundRecordService.confirmNoRefund(request);
+ return packageResponse(request.getCustomerPackageId());
+ }
+
+ private GaoCustomerPackage packageAfterFundAction(String fundRecordId) {
+ var record = fundRecordService.get(fundRecordId);
+ TimiException.required(record, "not found fund record");
+ return packageResponse(record.getBizId());
+ }
+
+ private GaoCustomerPackage packageResponse(String packageId) {
+ GaoCustomerPackage result = service.get(packageId);
+ TimiException.required(result, "not found customer package");
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ private void fillTransient(List packageList) {
+ if (packageList == null || packageList.isEmpty()) {
+ return;
+ }
+ Map> itemMap = service.mapItemListByCustomerPackageIdList(packageList.stream().map(GaoCustomerPackage::getId).toList());
+ List userIdList = packageList.stream()
+ .flatMap(packageEntity -> Stream.of(packageEntity.getOperatorUserId(), packageEntity.getCancelOperatorUserId()))
+ .filter(value -> value != null && !value.isBlank())
+ .distinct()
+ .toList();
+ Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
+ for (GaoCustomerPackage packageEntity : packageList) {
+ List itemList = itemMap.getOrDefault(packageEntity.getId(), List.of());
+ for (GaoCustomerPackageItem item : itemList) {
+ fillDerived(item);
+ }
+ packageEntity.setItemList(itemList);
+ packageEntity.setOperator(userMap.get(packageEntity.getOperatorUserId()));
+ packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId()));
+ }
+ }
+
+ private void fillDeliveryTransient(List deliveryList) {
+ if (deliveryList == null || deliveryList.isEmpty()) {
+ return;
+ }
+ List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList();
+ Map> itemMap = deliveryService.mapItemListByDeliveryIdList(idList);
+ Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList);
+ List userIdList = deliveryList.stream()
+ .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId()))
+ .filter(value -> value != null && !value.isBlank())
+ .distinct()
+ .toList();
+ Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
+ for (GaoPackageDelivery delivery : deliveryList) {
+ delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of()));
+ delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of()));
+ delivery.setOperator(userMap.get(delivery.getOperatorUserId()));
+ delivery.setVerifier(userMap.get(delivery.getVerifierUserId()));
+ }
+ }
+
+ private void fillFundTransient(List recordList) {
+ if (recordList == null || recordList.isEmpty()) {
+ return;
+ }
+ List idList = recordList.stream().map(GaoFundRecord::getId).toList();
+ Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList);
+ List userIdList = recordList.stream()
+ .flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId()))
+ .filter(value -> value != null && !value.isBlank())
+ .distinct()
+ .toList();
+ Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
+ for (GaoFundRecord record : recordList) {
+ record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
+ record.setOperator(userMap.get(record.getOperatorUserId()));
+ record.setVerifier(userMap.get(record.getVerifierUserId()));
+ }
+ }
+
+ private void fillDerived(GaoCustomerPackageItem item) {
+ BigDecimal reserved = item.getReservedQuantity() == null ? BigDecimal.ZERO : item.getReservedQuantity();
+ BigDecimal delivered = item.getDeliveredQuantity() == null ? BigDecimal.ZERO : item.getDeliveredQuantity();
+ BigDecimal total = item.getTotalQuantity() == null ? BigDecimal.ZERO : item.getTotalQuantity();
+ item.setPendingQuantity(reserved);
+ item.setAvailableQuantity(total.subtract(reserved).subtract(delivered));
+ item.setRemainingQuantity(total.subtract(delivered));
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java
index 0e86a79..1cd87f1 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java
@@ -4,7 +4,10 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
+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.GaoStoreScopeService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
@@ -32,6 +35,7 @@ import java.util.Map;
public class GaoEventController {
private final GaoEventService service;
+ private final GaoPackageConfigService packageConfigService;
private final GaoStoreScopeService storeScopeService;
@AOPLog
@@ -40,26 +44,67 @@ public class GaoEventController {
@PostMapping("/list")
public PageResult list(@RequestBody Page page) {
TimiException.required(page, "not found page");
+ resolveStoreScope(page);
+ PageResult result = service.pageWithoutDeleted(page);
+ return attachEventRelations(result);
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.EVENT_CREATE)
+ @PostMapping("/list/history")
+ public PageResult listHistory(@RequestBody Page page) {
+ TimiException.required(page, "not found page");
+ resolveStoreScope(page);
+ return attachEventRelations(service.page(page));
+ }
+
+ private void resolveStoreScope(Page page) {
if (!storeScopeService.hasGlobalScope()) {
GaoEvent example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoEvent());
example.setStoreId(storeScopeService.resolveStoreId(example.getStoreId()));
page.setEqualsExample(example);
}
- PageResult result = service.page(page);
+ }
+
+ private PageResult attachEventRelations(PageResult result) {
Map> roleMap = service.mapRoleCodeListByEventIdList(result.getList().stream().map(GaoEvent::getId).toList());
for (GaoEvent event : result.getList()) {
event.setRoleCodeList(roleMap.get(event.getId()));
}
service.attachOptionGroupList(result.getList());
+ attachPackageConfig(result.getList());
return result;
}
+ private void attachPackageConfig(List eventList) {
+ List packageConfigIdList = eventList.stream()
+ .map(GaoEvent::getPackageConfigId)
+ .filter(TimiJava::isNotEmpty)
+ .distinct()
+ .toList();
+ if (packageConfigIdList.isEmpty()) {
+ return;
+ }
+ Map packageConfigMap = packageConfigService.mapByIdList(packageConfigIdList);
+ Map> itemMap = packageConfigService.mapItemListByPackageConfigIdList(packageConfigIdList);
+ for (GaoEvent event : eventList) {
+ GaoPackageConfig packageConfig = packageConfigMap.get(event.getPackageConfigId());
+ if (packageConfig != null) {
+ packageConfig.setItemList(itemMap.getOrDefault(packageConfig.getId(), List.of()));
+ event.setPackageConfig(packageConfig);
+ }
+ }
+ }
+
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_READ)
@PostMapping("/list/valid")
public List listValid(@RequestParam String storeId) {
- return service.listValid(storeScopeService.resolveStoreId(storeId));
+ List result = service.listValid(storeScopeService.resolveStoreId(storeId));
+ attachPackageConfig(result);
+ return result;
}
@AOPLog
@@ -72,6 +117,7 @@ public class GaoEventController {
storeScopeService.checkStoreId(event.getStoreId());
event.setRoleCodeList(service.listRoleCode(event.getId()));
service.attachOptionGroupList(List.of(event));
+ attachPackageConfig(List.of(event));
return event;
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java
index 4361af7..4a4073f 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java
@@ -13,10 +13,10 @@ import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView;
+import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPageResult;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
-import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
@@ -114,6 +114,7 @@ public class GaoEventRecordController {
record.setEvent(eventService.get(record.getEventId()));
eventService.attachOptionGroupList(List.of(record.getEvent()));
record.setOperator(userService.get(record.getOperatorUserId()));
+ record.setUpdater(userService.get(record.getUpdatedBy()));
if (record.getVerifierUserId() != null) {
record.setVerifier(userService.get(record.getVerifierUserId()));
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java
new file mode 100644
index 0000000..11686dc
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java
@@ -0,0 +1,95 @@
+package com.imyeyu.api.modules.gao.controller;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.common.service.AttachmentService;
+import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
+import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
+import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
+import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
+import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
+import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.annotation.AOPLog;
+import com.imyeyu.spring.annotation.RequestRateLimit;
+import com.imyeyu.spring.bean.PageResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/// GAO 资金流水接口
+///
+/// @author Codex
+/// @since 2026-08-31
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/gao/fund/record")
+public class GaoFundRecordController {
+
+ private final GaoFundRecordService service;
+ private final GaoStoreScopeService storeScopeService;
+ private final AttachmentService attachmentService;
+ private final UserService userService;
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ)
+ @PostMapping("/list")
+ public PageResult list(@RequestBody GaoFundRecordPage page) {
+ TimiException.required(page, "not found page");
+ page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
+ PageResult result = service.pageByQuery(page);
+ fillTransient(result.getList());
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ)
+ @PostMapping("/detail")
+ public GaoFundRecord detail(@RequestParam String id) {
+ GaoFundRecord result = service.get(id);
+ TimiException.required(result, "not found fund record");
+ storeScopeService.checkStoreId(result.getStoreId());
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE)
+ @PostMapping("/revoke")
+ public GaoFundRecord revoke(@RequestBody GaoFundRevokeRequest request) {
+ GaoFundRecord result = service.revoke(request);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ private void fillTransient(List recordList) {
+ if (recordList == null || recordList.isEmpty()) {
+ return;
+ }
+ List idList = recordList.stream().map(GaoFundRecord::getId).toList();
+ Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList);
+ List userIdList = recordList.stream()
+ .flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId()))
+ .filter(value -> value != null && !value.isBlank())
+ .distinct()
+ .toList();
+ Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
+ for (GaoFundRecord record : recordList) {
+ record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
+ record.setOperator(userMap.get(record.getOperatorUserId()));
+ record.setVerifier(userMap.get(record.getVerifierUserId()));
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java
new file mode 100644
index 0000000..a638a19
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java
@@ -0,0 +1,121 @@
+package com.imyeyu.api.modules.gao.controller;
+
+import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
+import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
+import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
+import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.annotation.AOPLog;
+import com.imyeyu.spring.annotation.RequestRateLimit;
+import com.imyeyu.spring.bean.PageResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+/// GAO 套餐配置接口
+///
+/// @author Codex
+/// @since 2026-08-31
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/gao/package/config")
+public class GaoPackageConfigController {
+
+ private final GaoPackageConfigService service;
+ private final GaoStoreScopeService storeScopeService;
+ private final UserService userService;
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ)
+ @PostMapping("/list")
+ public PageResult list(@RequestBody GaoPackageConfigPage page) {
+ TimiException.required(page, "not found page");
+ page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
+ PageResult result = service.pageByQuery(page);
+ fillTransient(result.getList());
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ)
+ @PostMapping("/list/valid")
+ public List listValid(@RequestParam String storeId) {
+ List result = service.listValid(storeScopeService.resolveStoreId(storeId));
+ fillTransient(result);
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ)
+ @PostMapping("/detail")
+ public GaoPackageConfig detail(@RequestParam String id) {
+ GaoPackageConfig config = service.get(id);
+ TimiException.required(config, "not found package config");
+ storeScopeService.checkStoreId(config.getStoreId());
+ fillTransient(List.of(config));
+ return config;
+ }
+
+ private void fillTransient(List configList) {
+ if (configList == null || configList.isEmpty()) {
+ return;
+ }
+ Map> itemMap = service.mapItemListByPackageConfigIdList(configList.stream().map(GaoPackageConfig::getId).toList());
+ List updaterIdList = configList.stream().map(GaoPackageConfig::getUpdatedBy).filter(value -> value != null && !value.isBlank()).distinct().toList();
+ Map updaterMap = updaterIdList.isEmpty() ? Map.of() : userService.mapByIdList(updaterIdList);
+ for (GaoPackageConfig config : configList) {
+ config.setItemList(itemMap.getOrDefault(config.getId(), List.of()));
+ config.setUpdater(updaterMap.get(config.getUpdatedBy()));
+ }
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_CREATE)
+ @PostMapping("/create")
+ public String create(@RequestBody GaoPackageConfig config) {
+ TimiException.required(config, "not found package config");
+ config.setStoreId(storeScopeService.resolveStoreId(config.getStoreId()));
+ service.create(config);
+ return config.getId();
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_UPDATE)
+ @PostMapping("/update")
+ public void update(@RequestBody GaoPackageConfig config) {
+ TimiException.required(config, "not found package config");
+ TimiException.required(config.getId(), "not found packageConfig.id");
+ GaoPackageConfig dbConfig = service.get(config.getId());
+ TimiException.required(dbConfig, "not found package config");
+ storeScopeService.checkStoreId(dbConfig.getStoreId());
+ config.setStoreId(dbConfig.getStoreId());
+ service.update(config);
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_DELETE)
+ @PostMapping("/delete")
+ public void delete(@RequestParam String id) {
+ GaoPackageConfig config = service.get(id);
+ TimiException.required(config, "not found package config");
+ storeScopeService.checkStoreId(config.getStoreId());
+ service.delete(id);
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java
new file mode 100644
index 0000000..716e04b
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java
@@ -0,0 +1,140 @@
+package com.imyeyu.api.modules.gao.controller;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.common.service.AttachmentService;
+import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
+import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
+import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService;
+import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.annotation.AOPLog;
+import com.imyeyu.spring.annotation.RequestRateLimit;
+import com.imyeyu.spring.bean.PageResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/// GAO 套餐发放接口
+///
+/// @author Codex
+/// @since 2026-08-31
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/gao/package/delivery")
+public class GaoPackageDeliveryController {
+
+ private final GaoPackageDeliveryService service;
+ private final GaoStoreScopeService storeScopeService;
+ private final AttachmentService attachmentService;
+ private final UserService userService;
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ)
+ @PostMapping("/list")
+ public PageResult list(@RequestBody GaoPackageDeliveryPage page) {
+ TimiException.required(page, "not found page");
+ page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
+ PageResult result = service.pageByQuery(page);
+ fillTransient(result.getList());
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ)
+ @PostMapping("/detail")
+ public GaoPackageDelivery detail(@RequestParam String id) {
+ GaoPackageDelivery result = service.get(id);
+ TimiException.required(result, "not found delivery");
+ storeScopeService.checkStoreId(result.getStoreId());
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE)
+ @PostMapping("/create")
+ public GaoPackageDelivery create(@RequestBody GaoPackageDeliveryCreateRequest request) {
+ GaoPackageDelivery result = service.createDelivery(request);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY)
+ @PostMapping("/verify")
+ public GaoPackageDelivery verify(@RequestParam String id) {
+ GaoPackageDelivery result = service.verify(id);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY)
+ @PostMapping("/reject")
+ public GaoPackageDelivery reject(@RequestBody GaoPackageDeliveryActionRequest request) {
+ GaoPackageDelivery result = service.reject(request);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE)
+ @PostMapping("/cancel")
+ public GaoPackageDelivery cancel(@RequestBody GaoPackageDeliveryActionRequest request) {
+ GaoPackageDelivery result = service.cancel(request);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_REVOKE)
+ @PostMapping("/revoke")
+ public GaoPackageDelivery revoke(@RequestBody GaoPackageDeliveryRevokeRequest request) {
+ GaoPackageDelivery result = service.revoke(request);
+ fillTransient(List.of(result));
+ return result;
+ }
+
+ private void fillTransient(List deliveryList) {
+ if (deliveryList == null || deliveryList.isEmpty()) {
+ return;
+ }
+ List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList();
+ Map> itemMap = service.mapItemListByDeliveryIdList(idList);
+ Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList);
+ List userIdList = deliveryList.stream()
+ .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId()))
+ .filter(value -> value != null && !value.isBlank())
+ .distinct()
+ .toList();
+ Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
+ for (GaoPackageDelivery delivery : deliveryList) {
+ delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of()));
+ delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of()));
+ delivery.setOperator(userMap.get(delivery.getOperatorUserId()));
+ delivery.setVerifier(userMap.get(delivery.getVerifierUserId()));
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreDiaryController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreDiaryController.java
new file mode 100644
index 0000000..aa30554
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreDiaryController.java
@@ -0,0 +1,129 @@
+package com.imyeyu.api.modules.gao.controller;
+
+import com.fasterxml.jackson.annotation.JsonView;
+import com.imyeyu.api.modules.common.entity.Article;
+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.service.GaoStoreDiaryService;
+import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.api.modules.user.service.UserLoginService;
+import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.java.TimiJava;
+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 com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage;
+import com.imyeyu.spring.util.ResponseView;
+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-08-31
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/gao/store/diary")
+public class GaoStoreDiaryController {
+
+ private final GaoStoreDiaryService service;
+ private final GaoStoreScopeService storeScopeService;
+ private final UserLoginService userLoginService;
+ private final UserService userService;
+ private final AttachmentService attachmentService;
+
+ /// 分页查询门店日记
+ ///
+ /// @param page 分页查询条件
+ /// @return 门店日记分页结果
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.STORE_DIARY_READ)
+ @PostMapping("/list")
+ @JsonView(ResponseView.Public.class)
+ public PageResult list(@RequestBody GaoStoreDiaryPage page) {
+ TimiException.required(page, "未找到分页参数");
+ TimiException.required(page.getBeginCreatedAt(), "未找到门店日记开始时间");
+ TimiException.required(page.getEndCreatedAt(), "未找到门店日记结束时间");
+ TimiException.requiredTrue(page.getBeginCreatedAt() < page.getEndCreatedAt(), "门店日记时间范围无效");
+ page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
+ return service.page(page);
+ }
+
+ /// 查询门店日记详情
+ ///
+ /// @param id 日记 ID
+ /// @return 门店日记
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.STORE_DIARY_READ)
+ @PostMapping("/detail")
+ @JsonView(ResponseView.Public.class)
+ public Article detail(@RequestParam String id) {
+ Article article = service.get(id);
+ storeScopeService.checkStoreId(article.getBizId());
+ article.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId()));
+ if (TimiJava.isNotEmpty(article.getUpdatedBy())) {
+ User updater = userService.get(article.getUpdatedBy());
+ if (updater != null) {
+ updater.setAttachmentList(attachmentService.listByBizId(
+ Attachment.BizType.USER,
+ updater.getId(),
+ User.AttachType.AVATAR.toString()
+ ));
+ }
+ article.setUpdater(updater);
+ }
+ return article;
+ }
+
+ /// 创建门店日记
+ ///
+ /// @param article 门店日记
+ /// @return 日记 ID
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.STORE_DIARY_CREATE)
+ @PostMapping("/create")
+ public String create(@RequestBody Article article) {
+ article.setBizId(storeScopeService.resolveStoreId(article.getBizId()));
+ service.create(article, userLoginService.getRequireLoginUserId());
+ return article.getId();
+ }
+
+ /// 修改门店日记
+ ///
+ /// @param article 门店日记
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.STORE_DIARY_UPDATE)
+ @PostMapping("/update")
+ public void update(@RequestBody Article article) {
+ TimiException.required(article, "未找到门店日记");
+ TimiException.required(article.getId(), "未找到门店日记 ID");
+ Article dbArticle = service.get(article.getId());
+ storeScopeService.checkStoreId(dbArticle.getBizId());
+ service.update(article, userLoginService.getRequireLoginUserId());
+ }
+
+ /// 删除门店日记
+ ///
+ /// @param id 日记 ID
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.STORE_DIARY_DELETE)
+ @PostMapping("/delete")
+ public void delete(@RequestParam String id) {
+ Article article = service.get(id);
+ storeScopeService.checkStoreId(article.getBizId());
+ service.delete(article.getId());
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java
new file mode 100644
index 0000000..facd096
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java
@@ -0,0 +1,145 @@
+package com.imyeyu.api.modules.gao.entity;
+
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.spring.annotation.table.Transient;
+import com.imyeyu.spring.entity.UUIDEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/// GAO 客户实际购买的套餐
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoCustomerPackage extends UUIDEntity {
+
+ /// 客户套餐状态
+ public enum Status {
+
+ /// 履约中
+ ACTIVE,
+
+ /// 已全部履约
+ FULFILLED,
+
+ /// 已取消
+ CANCELLED
+ }
+
+ /// 付款状态
+ public enum PaymentStatus {
+
+ /// 未付款
+ UNPAID,
+
+ /// 部分付款
+ PARTIALLY_PAID,
+
+ /// 已付清
+ PAID
+ }
+
+ /// 退款状态
+ public enum RefundStatus {
+
+ /// 无需退款
+ NOT_REQUIRED,
+
+ /// 待退款或待确认无需退款
+ PENDING,
+
+ /// 已完成退款处理
+ REFUNDED
+ }
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 客户 ID
+ private String customerId;
+
+ /// 来源套餐配置 ID
+ private String packageConfigId;
+
+ /// 购买事件登记 ID
+ private String purchaseEventRecordId;
+
+ /// 购买幂等请求号
+ private String requestNo;
+
+ /// 套餐名称快照
+ private String packageNameSnapshot;
+
+ /// 标准价快照,单位分
+ private Long standardAmountSnapshot;
+
+ /// 实际成交价,单位分
+ private Long saleAmount;
+
+ /// 待核销收款合计,单位分
+ private Long pendingPaidAmount;
+
+ /// 累计有效收款,单位分
+ private Long paidAmount;
+
+ /// 累计有效退款,单位分
+ private Long refundedAmount;
+
+ /// 套餐状态
+ private Status status;
+
+ /// 付款状态
+ private PaymentStatus paymentStatus;
+
+ /// 退款状态
+ private RefundStatus refundStatus;
+
+ /// 购买时间
+ private Long purchasedAt;
+
+ /// 购买登记人
+ private String operatorUserId;
+
+ /// 取消时间
+ private Long cancelledAt;
+
+ /// 取消操作人
+ private String cancelOperatorUserId;
+
+ /// 取消原因
+ private String cancelReason;
+
+ /// 备注
+ private String remark;
+
+ /// 客户
+ @Transient
+ private GaoCustomer customer;
+
+ /// 购买事件登记
+ @Transient
+ private GaoEventRecord purchaseEventRecord;
+
+ /// 购买操作人
+ @Transient
+ private User operator;
+
+ /// 取消操作人
+ @Transient
+ private User cancelOperator;
+
+ /// 产品权益列表
+ @Transient
+ private List itemList;
+
+ /// 发放历史
+ @Transient
+ private List deliveryList;
+
+ /// 资金历史
+ @Transient
+ private List fundRecordList;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java
new file mode 100644
index 0000000..450f06a
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java
@@ -0,0 +1,53 @@
+package com.imyeyu.api.modules.gao.entity;
+
+import com.imyeyu.spring.annotation.table.Transient;
+import com.imyeyu.spring.entity.UUIDEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/// GAO 客户套餐产品权益快照
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoCustomerPackageItem extends UUIDEntity {
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 产品名称快照
+ private String productNameSnapshot;
+
+ /// 产品单位快照
+ private String unitSnapshot;
+
+ /// 总权益数量
+ private BigDecimal totalQuantity;
+
+ /// 待核销占用数量
+ private BigDecimal reservedQuantity;
+
+ /// 已核销净发放数量
+ private BigDecimal deliveredQuantity;
+
+ /// 排序值
+ private Integer sort;
+
+ /// 备注
+ private String remark;
+
+ /// 待核销数量
+ @Transient
+ private BigDecimal pendingQuantity;
+
+ /// 当前可发数量
+ @Transient
+ private BigDecimal availableQuantity;
+
+ /// 权益剩余数量
+ @Transient
+ private BigDecimal remainingQuantity;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java
index 1eb6102..c48b270 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java
@@ -18,6 +18,16 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public class GaoEvent extends UUIDEntity {
+ /// 事件业务类型
+ public enum BizType {
+
+ /// 普通登记事件
+ GENERAL,
+
+ /// 购买产品套餐
+ PACKAGE_PURCHASE
+ }
+
///
/// 登记事件状态
///
@@ -93,6 +103,16 @@ public class GaoEvent extends UUIDEntity {
/// 统计图表颜色
private String color;
+ /// 业务类型
+ private BizType bizType;
+
+ /// 购买套餐事件绑定的套餐配置 ID
+ private String packageConfigId;
+
+ /// 购买套餐事件绑定的套餐配置
+ @Transient
+ private GaoPackageConfig packageConfig;
+
/// 登记事件名称
private String name;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java
index 410ce81..581a961 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java
@@ -87,6 +87,9 @@ public class GaoEventRecord extends UUIDEntity {
/// 营业日递增序号
private Long businessDayNo;
+ /// 最后修改用户 ID
+ private String updatedBy;
+
@Transient
private GaoCustomer customer;
@@ -99,6 +102,10 @@ public class GaoEventRecord extends UUIDEntity {
@Transient
private User verifier;
+ /// 最后修改用户
+ @Transient
+ private User updater;
+
@Transient
private List attachmentList;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java
new file mode 100644
index 0000000..7292ca0
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java
@@ -0,0 +1,115 @@
+package com.imyeyu.api.modules.gao.entity;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.spring.annotation.table.Transient;
+import com.imyeyu.spring.entity.UUIDEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/// GAO 通用资金操作流水
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoFundRecord extends UUIDEntity {
+
+ /// 资金业务类型
+ public enum BizType {
+
+ /// 客户套餐
+ CUSTOMER_PACKAGE
+ }
+
+ /// 资金操作类型
+ public enum OperationType {
+
+ /// 线下收款
+ RECEIVE,
+
+ /// 退款
+ REFUND,
+
+ /// 确认无需退款
+ CONFIRM_NO_REFUND,
+
+ /// 资金操作冲正
+ REVOKE
+ }
+
+ /// 资金操作状态
+ public enum Status {
+
+ /// 待店长核销
+ PENDING,
+
+ /// 已生效
+ VERIFIED,
+
+ /// 店长已拒绝
+ REJECTED,
+
+ /// 提交人已撤销
+ CANCELLED
+ }
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 客户 ID
+ private String customerId;
+
+ /// 资金业务类型
+ private BizType bizType;
+
+ /// 业务对象 ID
+ private String bizId;
+
+ /// 操作类型
+ private OperationType operationType;
+
+ /// 操作状态
+ private Status status;
+
+ /// 金额,单位分
+ private Long amount;
+
+ /// 冲正时关联原资金记录
+ private String sourceRecordId;
+
+ /// 幂等请求号
+ private String requestNo;
+
+ /// 提交操作的用户 ID
+ private String operatorUserId;
+
+ /// 收款核销用户 ID
+ private String verifierUserId;
+
+ /// 收款核销时间
+ private Long verifiedAt;
+
+ /// 线下操作发生时间
+ private Long occurredAt;
+
+ /// 原因
+ private String reason;
+
+ /// 备注
+ private String remark;
+
+ /// 操作人
+ @Transient
+ private User operator;
+
+ /// 核销人
+ @Transient
+ private User verifier;
+
+ /// 附件
+ @Transient
+ private List attachmentList;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfig.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfig.java
new file mode 100644
index 0000000..406fe5c
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfig.java
@@ -0,0 +1,66 @@
+package com.imyeyu.api.modules.gao.entity;
+
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.spring.annotation.table.Transient;
+import com.imyeyu.spring.entity.UUIDEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/// GAO 套餐配置
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoPackageConfig extends UUIDEntity {
+
+ /// 套餐配置状态
+ public enum Status {
+
+ /// 草稿,不可销售
+ DRAFT,
+
+ /// 启用,可在有效期内销售
+ ACTIVE,
+
+ /// 停用,不可销售
+ INACTIVE
+ }
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 套餐名称
+ private String name;
+
+ /// 套餐说明
+ private String description;
+
+ /// 标准总价,单位分
+ private Long standardAmount;
+
+ /// 状态
+ private Status status;
+
+ /// 生效开始时间戳,毫秒,空为不限制
+ private Long beginAt;
+
+ /// 生效结束时间戳,毫秒,空为不限制
+ private Long endAt;
+
+ /// 排序值
+ private Integer sort;
+
+ /// 最后修改用户 ID
+ private String updatedBy;
+
+ /// 最后修改用户
+ @Transient
+ private User updater;
+
+ /// 配置产品列表
+ @Transient
+ private List itemList;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java
new file mode 100644
index 0000000..205d250
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java
@@ -0,0 +1,34 @@
+package com.imyeyu.api.modules.gao.entity;
+
+import com.imyeyu.spring.entity.UUIDEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/// GAO 套餐配置产品明细
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoPackageConfigItem extends UUIDEntity {
+
+ /// 套餐配置 ID
+ private String packageConfigId;
+
+ /// 产品名称
+ private String productName;
+
+ /// 产品单位
+ private String unit;
+
+ /// 默认数量
+ private BigDecimal defaultQuantity;
+
+ /// 排序值
+ private Integer sort;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java
new file mode 100644
index 0000000..34f2b6e
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java
@@ -0,0 +1,107 @@
+package com.imyeyu.api.modules.gao.entity;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.spring.annotation.table.Transient;
+import com.imyeyu.spring.entity.UUIDEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/// GAO 套餐发放操作
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoPackageDelivery extends UUIDEntity {
+
+ /// 发放操作类型
+ public enum OperationType {
+
+ /// 正常发放
+ DELIVER,
+
+ /// 整单冲正
+ REVOKE
+ }
+
+ /// 发放状态
+ public enum Status {
+
+ /// 待核销
+ PENDING,
+
+ /// 已核销
+ VERIFIED,
+
+ /// 店长已拒绝
+ REJECTED,
+
+ /// 提交人已撤销
+ CANCELLED,
+
+ /// 已核销后被整单冲正
+ REVOKED
+ }
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 客户 ID
+ private String customerId;
+
+ /// 操作类型
+ private OperationType operationType;
+
+ /// 发放状态
+ private Status status;
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 提交人
+ private String operatorUserId;
+
+ /// 实际发放时间
+ private Long deliveredAt;
+
+ /// 核销或拒绝操作人
+ private String verifierUserId;
+
+ /// 核销或拒绝时间
+ private Long verifiedAt;
+
+ /// 冲正单指向原发放单
+ private String sourceDeliveryId;
+
+ /// 拒绝、撤销或冲正原因
+ private String reason;
+
+ /// 备注
+ private String remark;
+
+ /// 操作人
+ @Transient
+ private User operator;
+
+ /// 核销或拒绝操作人
+ @Transient
+ private User verifier;
+
+ /// 原发放单
+ @Transient
+ private GaoPackageDelivery sourceDelivery;
+
+ /// 发放明细
+ @Transient
+ private List itemList;
+
+ /// 附件
+ @Transient
+ private List attachmentList;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java
new file mode 100644
index 0000000..f1fab81
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java
@@ -0,0 +1,43 @@
+package com.imyeyu.api.modules.gao.entity;
+
+import com.imyeyu.spring.entity.UUIDEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/// GAO 套餐发放产品明细
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoPackageDeliveryItem extends UUIDEntity {
+
+ /// 发放单 ID
+ private String deliveryId;
+
+ /// 客户套餐产品 ID
+ private String customerPackageItemId;
+
+ /// 本次数量
+ private BigDecimal quantity;
+
+ /// 操作前占用数量快照
+ private BigDecimal reservedBefore;
+
+ /// 操作后占用数量快照
+ private BigDecimal reservedAfter;
+
+ /// 操作前已发数量快照
+ private BigDecimal deliveredBefore;
+
+ /// 操作后已发数量快照
+ private BigDecimal deliveredAfter;
+
+ /// 产品名称快照
+ private String productNameSnapshot;
+
+ /// 产品单位快照
+ private String unitSnapshot;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java
new file mode 100644
index 0000000..79f9ba4
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java
@@ -0,0 +1,30 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
+import com.imyeyu.spring.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Collection;
+import java.util.List;
+
+/// GAO 客户套餐产品权益 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoCustomerPackageItemMapper extends BaseMapper {
+
+ /// 按客户套餐查询产品权益
+ List selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
+
+ /// 批量查询客户套餐产品权益
+ List selectByCustomerPackageIdList(@Param("customerPackageIdList") Collection customerPackageIdList);
+
+ /// 锁定客户套餐产品
+ GaoCustomerPackageItem selectForUpdate(@Param("id") String id);
+
+ /// 调整待核销占用数量
+ int updateReservedQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta);
+
+ /// 调整已核销发放数量
+ int updateDeliveredQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java
new file mode 100644
index 0000000..39ac742
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java
@@ -0,0 +1,40 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
+import com.imyeyu.spring.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+/// GAO 客户套餐 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoCustomerPackageMapper extends BaseMapper {
+
+ /// 按门店和请求号查询购买结果
+ GaoCustomerPackage selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
+
+ /// 按请求号查询购买结果,用于配置删除后的幂等重试
+ GaoCustomerPackage selectByRequestNoAnyStore(@Param("requestNo") String requestNo);
+
+ /// 锁定客户套餐
+ GaoCustomerPackage selectForUpdate(@Param("id") String id);
+
+ /// 按购买事件登记查询客户套餐
+ GaoCustomerPackage selectByPurchaseEventRecordId(@Param("purchaseEventRecordId") String purchaseEventRecordId);
+
+ /// 按条件统计客户套餐
+ long countByQuery(GaoCustomerPackagePage page);
+
+ /// 按条件查询客户套餐
+ java.util.List selectByQuery(GaoCustomerPackagePage page);
+
+ /// 原子调整待核销收款
+ int updatePendingPaidAmountDelta(@Param("id") String id, @Param("delta") Long delta);
+
+ /// 原子调整有效收款
+ int updatePaidAmountDelta(@Param("id") String id, @Param("delta") Long delta);
+
+ /// 带状态条件更新套餐状态
+ int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoCustomerPackage.Status fromStatus, @Param("toStatus") GaoCustomerPackage.Status toStatus, @Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId, @Param("cancelReason") String cancelReason, @Param("refundStatus") GaoCustomerPackage.RefundStatus refundStatus);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java
index 259b345..23e1696 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java
@@ -1,8 +1,11 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
+import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.SelectProvider;
import java.util.Collection;
import java.util.List;
@@ -14,7 +17,16 @@ import java.util.List;
/// @since 2026-07-27 15:53
public interface GaoEventMapper extends BaseMapper {
+ @SelectProvider(type = GaoEventSQLProvider.class, method = "selectPageWithoutDeleted")
+ List selectPageWithoutDeleted(Page page);
+
+ @SelectProvider(type = GaoEventSQLProvider.class, method = "countPageWithoutDeleted")
+ long countPageWithoutDeleted(Page page);
+
List selectByIdList(Collection idList);
List selectValidList(@Param("storeId") String storeId, @Param("now") Long now);
+
+ @Select("SELECT COUNT(*) FROM `gao_event` WHERE `package_config_id` = #{packageConfigId} AND `deleted_at` IS NULL")
+ long countByPackageConfigId(String packageConfigId);
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java
index b829bbd..6b473da 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java
@@ -12,6 +12,7 @@ import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
import java.util.List;
@@ -21,6 +22,13 @@ import java.util.List;
/// @since 2026-07-27 14:11
public interface GaoEventRecordMapper extends BaseMapper {
+ /// 查询事件登记记录数量
+ ///
+ /// @param eventId 事件 ID
+ /// @return 登记记录数量
+ @Select("SELECT COUNT(*) FROM `gao_event_record` WHERE `event_id` = #{eventId} AND `deleted_at` IS NULL")
+ long countByEventId(String eventId);
+
Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt);
/// 查询事件中已携带数值的登记记录数量
@@ -50,8 +58,9 @@ public interface GaoEventRecordMapper extends BaseMapper
/// @param id 登记记录 ID
/// @param verifierUserId 核销用户 ID
/// @param verifiedAt 核销时间
+ /// @param updatedBy 最后修改用户 ID
/// @return 更新行数
- int verify(@Param("id") String id, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt);
+ int verify(@Param("id") String id, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("updatedBy") String updatedBy);
/// 按门店和登记日期统计登记记录数
///
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java
new file mode 100644
index 0000000..6777a3e
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java
@@ -0,0 +1,32 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoEvent;
+import com.imyeyu.spring.bean.Page;
+import com.imyeyu.spring.util.SQLProvider;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.builder.annotation.ProviderContext;
+
+/// 登记事件分页查询 SQL 提供器
+///
+/// @author Codex
+public class GaoEventSQLProvider extends SQLProvider {
+
+ /// 分页查询未删除的登记事件
+ public String selectPageWithoutDeleted(ProviderContext context, @Param("page") Page> page) {
+ EntityMeta meta = getEntityMeta(context);
+ return appendWithoutDeleted(buildSelectByPageSQL(meta, meta.getTable(), page, "#{offset}", "#{limit}"));
+ }
+
+ /// 统计未删除的登记事件
+ public String countPageWithoutDeleted(ProviderContext context, @Param("page") Page> page) {
+ EntityMeta meta = getEntityMeta(context);
+ return appendWithoutDeleted(buildCountByPageSQL(meta, meta.getTable(), page));
+ }
+
+ private String appendWithoutDeleted(String sql) {
+ return sql.replace(
+ "WHERE 1 = 1",
+ "WHERE 1 = 1 AND (`status` IS NULL OR `status` <> '%s')".formatted(GaoEvent.Status.DELETED.name())
+ );
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java
new file mode 100644
index 0000000..4f130b0
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java
@@ -0,0 +1,39 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
+import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
+import com.imyeyu.spring.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/// GAO 资金流水 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoFundRecordMapper extends BaseMapper {
+
+ /// 按门店和请求号查询资金操作
+ GaoFundRecord selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
+
+ /// 锁定资金记录
+ GaoFundRecord selectForUpdate(@Param("id") String id);
+
+ /// 查询资金冲正记录
+ GaoFundRecord selectRevokeBySourceRecordId(@Param("sourceRecordId") String sourceRecordId);
+
+ /// 查询客户套餐待核销收款数量
+ long countPendingReceiveByBizId(@Param("bizId") String bizId);
+
+ /// 查询客户套餐资金历史
+ List selectByBizId(@Param("bizId") String bizId);
+
+ /// 按条件统计资金记录
+ long countByQuery(GaoFundRecordPage page);
+
+ /// 按条件查询资金记录
+ List selectByQuery(GaoFundRecordPage page);
+
+ /// 带状态条件更新资金记录
+ int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoFundRecord.Status fromStatus, @Param("toStatus") GaoFundRecord.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigItemMapper.java
new file mode 100644
index 0000000..a525b8b
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigItemMapper.java
@@ -0,0 +1,21 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
+import com.imyeyu.spring.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Collection;
+import java.util.List;
+
+/// GAO 套餐配置产品 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoPackageConfigItemMapper extends BaseMapper {
+
+ /// 按套餐配置 ID 查询产品
+ List selectByPackageConfigId(@Param("packageConfigId") String packageConfigId);
+
+ /// 批量查询套餐配置产品
+ List selectByPackageConfigIdList(@Param("packageConfigIdList") Collection packageConfigIdList);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigMapper.java
new file mode 100644
index 0000000..5d8ac9b
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageConfigMapper.java
@@ -0,0 +1,31 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
+import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
+import com.imyeyu.spring.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Collection;
+import java.util.List;
+
+/// GAO 套餐配置 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoPackageConfigMapper extends BaseMapper {
+
+ /// 按 ID 列表查询套餐配置
+ List selectByIdList(@Param("idList") Collection idList);
+
+ /// 查询有效可购买套餐
+ List selectValidList(@Param("storeId") String storeId, @Param("now") Long now);
+
+ /// 按条件统计套餐配置
+ long countByQuery(GaoPackageConfigPage page);
+
+ /// 按条件查询套餐配置
+ List selectByQuery(GaoPackageConfigPage page);
+
+ /// 查询门店内同名配置
+ GaoPackageConfig selectByStoreIdAndName(@Param("storeId") String storeId, @Param("name") String name, @Param("excludeId") String excludeId);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java
new file mode 100644
index 0000000..ebee2fb
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java
@@ -0,0 +1,21 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
+import com.imyeyu.spring.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Collection;
+import java.util.List;
+
+/// GAO 套餐发放明细 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoPackageDeliveryItemMapper extends BaseMapper {
+
+ /// 按发放单查询明细
+ List selectByDeliveryId(@Param("deliveryId") String deliveryId);
+
+ /// 批量查询发放明细
+ List selectByDeliveryIdList(@Param("deliveryIdList") Collection deliveryIdList);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java
new file mode 100644
index 0000000..7ee4923
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java
@@ -0,0 +1,42 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage;
+import com.imyeyu.spring.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/// GAO 套餐发放 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoPackageDeliveryMapper extends BaseMapper {
+
+ /// 按门店和请求号查询发放结果
+ GaoPackageDelivery selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
+
+ /// 锁定发放单
+ GaoPackageDelivery selectForUpdate(@Param("id") String id);
+
+ /// 查询原发放单的冲正记录
+ GaoPackageDelivery selectRevokeBySourceDeliveryId(@Param("sourceDeliveryId") String sourceDeliveryId);
+
+ /// 查询客户套餐待核销发放数量
+ long countPendingByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
+
+ /// 查询客户套餐已核销正常发放数量
+ long countVerifiedByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
+
+ /// 查询客户套餐发放历史
+ java.util.List selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
+
+ /// 按条件统计发放单
+ long countByQuery(GaoPackageDeliveryPage page);
+
+ /// 按条件查询发放单
+ List selectByQuery(GaoPackageDeliveryPage page);
+
+ /// 带状态条件更新发放单
+ int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoPackageDelivery.Status fromStatus, @Param("toStatus") GaoPackageDelivery.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreDiaryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreDiaryMapper.java
new file mode 100644
index 0000000..39912c9
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreDiaryMapper.java
@@ -0,0 +1,26 @@
+package com.imyeyu.api.modules.gao.mapper;
+
+import com.imyeyu.api.modules.common.entity.Article;
+import com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage;
+import com.imyeyu.spring.mapper.BaseMapper;
+
+import java.util.List;
+
+/// GAO 门店日记 Mapper
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoStoreDiaryMapper extends BaseMapper {
+
+ /// 查询月份内的门店日记
+ ///
+ /// @param page 月份分页查询请求
+ /// @return 门店日记列表
+ List selectByMonth(GaoStoreDiaryPage page);
+
+ /// 统计月份内的门店日记数量
+ ///
+ /// @param page 月份分页查询请求
+ /// @return 门店日记数量
+ long countByMonth(GaoStoreDiaryPage page);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java
new file mode 100644
index 0000000..55ba3b2
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java
@@ -0,0 +1,87 @@
+package com.imyeyu.api.modules.gao.service;
+
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.spring.service.BaseService;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/// GAO 客户套餐服务
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoCustomerPackageService extends BaseService {
+
+ /// 按条件查询客户套餐
+ PageResult pageByQuery(GaoCustomerPackagePage page);
+
+ /// 创建客户套餐及购买事件登记
+ GaoCustomerPackage purchase(GaoCustomerPackagePurchaseRequest request);
+
+ /// 取消客户套餐
+ GaoCustomerPackage cancel(GaoCustomerPackageCancelRequest request);
+
+ /// 锁定客户套餐
+ ///
+ /// @param id 客户套餐 ID
+ /// @return 已锁定的客户套餐,不存在时为空
+ GaoCustomerPackage getForUpdate(String id);
+
+ /// 查询客户套餐产品权益
+ ///
+ /// @param customerPackageId 客户套餐 ID
+ /// @return 产品权益列表
+ List listItemByCustomerPackageId(String customerPackageId);
+
+ /// 批量查询客户套餐产品权益
+ ///
+ /// @param customerPackageIdList 客户套餐 ID 列表
+ /// @return 按客户套餐 ID 分组的产品权益
+ Map> mapItemListByCustomerPackageIdList(Collection customerPackageIdList);
+
+ /// 锁定客户套餐产品权益
+ ///
+ /// @param itemId 客户套餐产品权益 ID
+ /// @return 已锁定的产品权益,不存在时为空
+ GaoCustomerPackageItem getItemForUpdate(String itemId);
+
+ /// 原子调整待核销发放占用数量
+ ///
+ /// @param itemId 客户套餐产品权益 ID
+ /// @param delta 调整量
+ /// @return 受影响行数
+ int updateItemReservedQuantity(String itemId, BigDecimal delta);
+
+ /// 原子调整已核销发放数量
+ ///
+ /// @param itemId 客户套餐产品权益 ID
+ /// @param delta 调整量
+ /// @return 受影响行数
+ int updateItemDeliveredQuantity(String itemId, BigDecimal delta);
+
+ /// 原子调整待核销收款金额
+ ///
+ /// @param id 客户套餐 ID
+ /// @param delta 调整量
+ /// @return 受影响行数
+ int updatePendingPaidAmountDelta(String id, Long delta);
+
+ /// 原子调整有效收款金额
+ ///
+ /// @param id 客户套餐 ID
+ /// @param delta 调整量
+ /// @return 受影响行数
+ int updatePaidAmountDelta(String id, Long delta);
+
+ /// 保存客户套餐聚合状态
+ ///
+ /// @param packageEntity 客户套餐聚合
+ void updateAggregateState(GaoCustomerPackage packageEntity);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java
index 7db0e8a..eeeed06 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java
@@ -41,6 +41,18 @@ public interface GaoEventRecordService extends BaseService {
+ /// 分页查询未删除的登记事件
+ ///
+ /// @param page 分页参数
+ /// @return 登记事件分页结果
+ PageResult pageWithoutDeleted(Page page);
+
Map mapByIdList(Collection idList);
Map> mapRoleCodeListByEventIdList(Collection eventIdList);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java
new file mode 100644
index 0000000..b90fb98
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java
@@ -0,0 +1,53 @@
+package com.imyeyu.api.modules.gao.service;
+
+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.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.service.BaseService;
+
+import java.util.List;
+
+/// GAO 通用资金流水服务
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoFundRecordService extends BaseService {
+
+ /// 按条件查询资金流水
+ PageResult pageByQuery(GaoFundRecordPage page);
+
+ /// 查询客户套餐资金历史
+ List listByCustomerPackageId(String customerPackageId);
+
+ /// 登记线下收款
+ GaoFundRecord receive(GaoFundReceiveRequest request);
+
+ /// 核销收款
+ GaoFundRecord verify(String fundRecordId);
+
+ /// 拒绝待核销收款
+ GaoFundRecord reject(GaoFundActionRequest request);
+
+ /// 撤销自己提交的待核销收款
+ GaoFundRecord cancel(GaoFundActionRequest request);
+
+ /// 登记退款
+ GaoFundRecord refund(GaoFundRefundRequest request);
+
+ /// 确认无需退款
+ GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request);
+
+ /// 冲正资金流水
+ GaoFundRecord revoke(GaoFundRevokeRequest request);
+
+ /// 统计客户套餐待核销收款数量
+ ///
+ /// @param bizId 业务 ID
+ /// @return 待核销收款数量
+ long countPendingReceiveByBizId(String bizId);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageConfigService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageConfigService.java
new file mode 100644
index 0000000..602d2d3
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageConfigService.java
@@ -0,0 +1,42 @@
+package com.imyeyu.api.modules.gao.service;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
+import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.spring.service.BaseService;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/// GAO 套餐配置服务
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoPackageConfigService extends BaseService {
+
+ /// 按条件查询套餐配置
+ PageResult pageByQuery(GaoPackageConfigPage page);
+
+ /// 查询有效可购买套餐
+ List listValid(String storeId);
+
+ /// 批量查询套餐配置
+ ///
+ /// @param idList 套餐配置 ID 列表
+ /// @return 按套餐配置 ID 映射的配置
+ Map mapByIdList(Collection idList);
+
+ /// 查询套餐配置产品明细
+ ///
+ /// @param packageConfigId 套餐配置 ID
+ /// @return 产品明细列表
+ List listItemByPackageConfigId(String packageConfigId);
+
+ /// 批量查询套餐配置产品明细
+ ///
+ /// @param packageConfigIdList 套餐配置 ID 列表
+ /// @return 按套餐配置 ID 分组的产品明细
+ Map> mapItemListByPackageConfigIdList(Collection packageConfigIdList);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java
new file mode 100644
index 0000000..078e2c5
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java
@@ -0,0 +1,60 @@
+package com.imyeyu.api.modules.gao.service;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.spring.service.BaseService;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/// GAO 套餐发放服务
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoPackageDeliveryService extends BaseService {
+
+ /// 按条件查询发放单
+ PageResult pageByQuery(GaoPackageDeliveryPage page);
+
+ /// 查询客户套餐发放历史
+ List listByCustomerPackageId(String customerPackageId);
+
+ /// 批量查询发放明细
+ ///
+ /// @param deliveryIdList 发放单 ID 列表
+ /// @return 按发放单 ID 分组的发放明细
+ Map> mapItemListByDeliveryIdList(Collection deliveryIdList);
+
+ /// 创建待核销发放单
+ GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request);
+
+ /// 核销发放单
+ GaoPackageDelivery verify(String deliveryId);
+
+ /// 拒绝待核销发放单
+ GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request);
+
+ /// 撤销自己提交的待核销发放单
+ GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request);
+
+ /// 整单冲正已核销发放单
+ GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request);
+
+ /// 统计客户套餐待核销发放单数量
+ ///
+ /// @param customerPackageId 客户套餐 ID
+ /// @return 待核销发放单数量
+ long countPendingByCustomerPackageId(String customerPackageId);
+
+ /// 统计客户套餐已核销正常发放单数量
+ ///
+ /// @param customerPackageId 客户套餐 ID
+ /// @return 已核销正常发放单数量
+ long countVerifiedByCustomerPackageId(String customerPackageId);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreDiaryService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreDiaryService.java
new file mode 100644
index 0000000..b01a3b7
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreDiaryService.java
@@ -0,0 +1,46 @@
+package com.imyeyu.api.modules.gao.service;
+
+import com.imyeyu.api.modules.common.entity.Article;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage;
+
+/// GAO 门店日记服务
+///
+/// @author Codex
+/// @since 2026-08-31
+public interface GaoStoreDiaryService {
+
+ /// 分页查询门店日记
+ ///
+ /// @param page 分页查询条件
+ /// @return 门店日记分页结果
+ PageResult page(GaoStoreDiaryPage page);
+
+ /// 查询门店日记详情
+ ///
+ /// @param id 日记 ID
+ /// @return 门店日记
+ Article get(String id);
+
+ /// 创建门店日记
+ ///
+ /// @param article 门店日记
+ /// @param userId 操作用户 ID
+ void create(Article article, String userId);
+
+ /// 修改门店日记
+ ///
+ /// @param article 门店日记
+ /// @param userId 操作用户 ID
+ void update(Article article, String userId);
+
+ /// 删除门店日记
+ ///
+ /// @param id 日记 ID
+ void delete(String id);
+
+ /// 删除门店下的全部日记
+ ///
+ /// @param storeId 门店 ID
+ void deleteByStoreId(String storeId);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java
new file mode 100644
index 0000000..78f8b6d
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java
@@ -0,0 +1,406 @@
+package com.imyeyu.api.modules.gao.service.implement;
+
+import com.imyeyu.api.bean.ModuleCode;
+import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
+import com.imyeyu.api.modules.gao.entity.GaoCustomer;
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
+import com.imyeyu.api.modules.gao.entity.GaoEvent;
+import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
+import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageItemMapper;
+import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper;
+import com.imyeyu.api.modules.gao.service.GaoCustomerService;
+import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
+import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
+import com.imyeyu.api.modules.gao.service.GaoEventService;
+import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
+import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService;
+import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
+import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
+import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem;
+import com.imyeyu.api.modules.system.entity.Logger;
+import com.imyeyu.api.modules.system.service.LoggerService;
+import com.imyeyu.api.modules.user.service.PermissionChecker;
+import com.imyeyu.api.modules.user.service.UserLoginService;
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.spring.mapper.BaseMapper;
+import com.imyeyu.spring.service.AbstractEntityService;
+import com.imyeyu.utils.Time;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/// GAO 客户套餐服务实现
+///
+/// @author Codex
+/// @since 2026-08-31
+@Service
+@RequiredArgsConstructor
+public class GaoCustomerPackageServiceImplement extends AbstractEntityService implements GaoCustomerPackageService {
+
+ private static final BigDecimal ZERO = BigDecimal.ZERO;
+
+ private final GaoCustomerPackageMapper mapper;
+ private final GaoCustomerPackageItemMapper itemMapper;
+ private final ObjectProvider deliveryServiceProvider;
+ private final ObjectProvider fundRecordServiceProvider;
+ private final GaoPackageConfigService packageConfigService;
+ private final GaoCustomerService customerService;
+ private final GaoEventService eventService;
+ private final GaoEventRecordService eventRecordService;
+ private final GaoStoreScopeService storeScopeService;
+ private final UserLoginService userLoginService;
+ private final PermissionChecker permissionChecker;
+ private final LoggerService loggerService;
+
+ @Override
+ protected BaseMapper mapper() {
+ return mapper;
+ }
+
+ @Override
+ public GaoCustomerPackage getForUpdate(String id) {
+ return mapper.selectForUpdate(id);
+ }
+
+ @Override
+ public List listItemByCustomerPackageId(String customerPackageId) {
+ return itemMapper.selectByCustomerPackageId(customerPackageId);
+ }
+
+ @Override
+ public GaoCustomerPackageItem getItemForUpdate(String itemId) {
+ return itemMapper.selectForUpdate(itemId);
+ }
+
+ @Override
+ public int updateItemReservedQuantity(String itemId, BigDecimal delta) {
+ return itemMapper.updateReservedQuantity(itemId, delta);
+ }
+
+ @Override
+ public int updateItemDeliveredQuantity(String itemId, BigDecimal delta) {
+ return itemMapper.updateDeliveredQuantity(itemId, delta);
+ }
+
+ @Override
+ public int updatePendingPaidAmountDelta(String id, Long delta) {
+ return mapper.updatePendingPaidAmountDelta(id, delta);
+ }
+
+ @Override
+ public int updatePaidAmountDelta(String id, Long delta) {
+ return mapper.updatePaidAmountDelta(id, delta);
+ }
+
+ @Override
+ public void updateAggregateState(GaoCustomerPackage packageEntity) {
+ mapper.update(packageEntity);
+ }
+
+ /// 客户套餐必须通过购买流程创建
+ ///
+ /// @param packageEntity 客户套餐
+ @Override
+ public void create(GaoCustomerPackage packageEntity) {
+ TimiException.requiredTrue(false, "客户套餐必须通过购买接口创建");
+ }
+
+ /// 客户套餐创建后不允许通用修改
+ ///
+ /// @param packageEntity 客户套餐
+ @Override
+ public void update(GaoCustomerPackage packageEntity) {
+ TimiException.requiredTrue(false, "客户套餐不允许通用修改");
+ }
+
+ /// 客户套餐不允许通用删除
+ ///
+ /// @param id 客户套餐 ID
+ @Override
+ public void delete(String id) {
+ TimiException.requiredTrue(false, "客户套餐不允许删除");
+ }
+
+ /// 客户套餐不允许物理删除
+ ///
+ /// @param id 客户套餐 ID
+ @Override
+ public void destroy(String id) {
+ TimiException.requiredTrue(false, "客户套餐不允许物理删除");
+ }
+
+ @Override
+ public PageResult pageByQuery(GaoCustomerPackagePage page) {
+ TimiException.required(page, "not found page");
+ PageResult result = new PageResult<>();
+ result.setTotal(mapper.countByQuery(page));
+ result.setList(mapper.selectByQuery(page));
+ return result;
+ }
+
+ @Override
+ public GaoCustomerPackage get(String id) {
+ return super.get(id);
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoCustomerPackage purchase(GaoCustomerPackagePurchaseRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_PURCHASE", request, () -> purchaseInternal(request));
+ }
+
+ private GaoCustomerPackage purchaseInternal(GaoCustomerPackagePurchaseRequest request) {
+ TimiException.required(request, "not found purchase request");
+ TimiException.required(request.getRequestNo(), "not found requestNo");
+ request.setRequestNo(request.getRequestNo().trim());
+ TimiException.required(request.getEventId(), "not found eventId");
+ TimiException.required(request.getCustomerId(), "not found customerId");
+
+ GaoEvent event = validatePurchaseEvent(request.getEventId());
+ storeScopeService.checkStoreId(event.getStoreId());
+ GaoPackageConfig config = packageConfigService.get(event.getPackageConfigId());
+ TimiException.required(config, "购买事件绑定的套餐配置不存在");
+ TimiException.requiredTrue(event.getStoreId().equals(config.getStoreId()), "购买事件和套餐配置不属于同一门店");
+ List configItemList = packageConfigService.listItemByPackageConfigId(config.getId());
+ request.setSaleAmount(TimiJava.defaultIfNull(request.getSaleAmount(), config.getStandardAmount()));
+ TimiException.requiredTrue(0 <= request.getSaleAmount(), "成交价不能为负数");
+ if (request.getItemList() == null) {
+ request.setItemList(defaultPurchaseItems(configItemList));
+ }
+ List itemList = new java.util.ArrayList<>(request.getItemList());
+ verifyPurchaseItems(itemList);
+ itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort));
+ request.setItemList(itemList);
+
+ GaoCustomerPackage existing = mapper.selectByRequestNo(config.getStoreId(), request.getRequestNo());
+ if (existing != null) {
+ verifyPurchaseIdempotency(existing, request);
+ return existing;
+ }
+
+ Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
+ Long now = Time.now();
+ TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买");
+ TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效");
+ TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期");
+ GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId());
+ verifySameStructureAsConfig(itemList, configItemList);
+ boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO, com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
+ if (!canOverridePriceAndQuantity) {
+ TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价");
+ verifySameQuantityAsConfig(itemList, configItemList);
+ }
+
+ GaoEventRecord eventRecord = new GaoEventRecord();
+ eventRecord.setCustomerId(customer.getId());
+ eventRecord.setStoreId(config.getStoreId());
+ eventRecord.setEventId(event.getId());
+ eventRecord.setRegisteredAt(purchasedAt);
+ eventRecord.setRemark(request.getRemark());
+ if (event.getValueType() == GaoEvent.ValueType.NUMBER && event.getValueUnit() == GaoEvent.ValueUnit.CENT) {
+ eventRecord.setNumberValue(request.getSaleAmount());
+ }
+ GaoEventRecord createdEventRecord = eventRecordService.createPackagePurchaseRecord(eventRecordWithAttachments(eventRecord, request));
+
+ GaoCustomerPackage result = new GaoCustomerPackage();
+ result.setStoreId(config.getStoreId());
+ result.setCustomerId(customer.getId());
+ result.setPackageConfigId(config.getId());
+ result.setPurchaseEventRecordId(createdEventRecord.getId());
+ result.setRequestNo(request.getRequestNo());
+ result.setPackageNameSnapshot(config.getName());
+ result.setStandardAmountSnapshot(config.getStandardAmount());
+ result.setSaleAmount(request.getSaleAmount());
+ result.setPendingPaidAmount(0L);
+ result.setPaidAmount(0L);
+ result.setRefundedAmount(0L);
+ result.setStatus(GaoCustomerPackage.Status.ACTIVE);
+ result.setPaymentStatus(request.getSaleAmount() == 0 ? GaoCustomerPackage.PaymentStatus.PAID : GaoCustomerPackage.PaymentStatus.UNPAID);
+ result.setRefundStatus(GaoCustomerPackage.RefundStatus.NOT_REQUIRED);
+ result.setPurchasedAt(purchasedAt);
+ result.setOperatorUserId(userLoginService.getRequireLoginUserId());
+ result.setRemark(request.getRemark());
+ super.create(result);
+ for (GaoCustomerPackagePurchaseItem source : itemList) {
+ GaoCustomerPackageItem item = new GaoCustomerPackageItem();
+ item.setCustomerPackageId(result.getId());
+ item.setProductNameSnapshot(source.getProductName().trim());
+ item.setUnitSnapshot(source.getUnit().trim());
+ item.setTotalQuantity(source.getTotalQuantity());
+ item.setReservedQuantity(ZERO);
+ item.setDeliveredQuantity(ZERO);
+ item.setSort(source.getSort());
+ item.setRemark(source.getRemark());
+ itemMapper.insert(item);
+ }
+ return result;
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoCustomerPackage cancel(GaoCustomerPackageCancelRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_CANCEL", request, () -> cancelInternal(request));
+ }
+
+ private GaoCustomerPackage cancelInternal(GaoCustomerPackageCancelRequest request) {
+ TimiException.required(request, "not found cancel request");
+ TimiException.required(request.getCustomerPackageId(), "not found customerPackageId");
+ String reason = requiredReason(request.getReason(), "取消原因不能为空");
+ GaoCustomerPackage packageEntity = mapper.selectForUpdate(request.getCustomerPackageId());
+ TimiException.required(packageEntity, "not found customer package");
+ storeScopeService.checkStoreId(packageEntity.getStoreId());
+ TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可取消");
+ TimiException.requiredTrue(0 == deliveryServiceProvider.getObject().countPendingByCustomerPackageId(packageEntity.getId()), "存在待核销发放,不能取消套餐");
+ TimiException.requiredTrue(0 == fundRecordServiceProvider.getObject().countPendingReceiveByBizId(packageEntity.getId()), "存在待核销收款,不能取消套餐");
+ TimiException.requiredTrue(0 == pendingAmount(packageEntity), "存在待核销收款,不能取消套餐");
+ List itemList = itemMapper.selectByCustomerPackageId(packageEntity.getId());
+ for (GaoCustomerPackageItem item : itemList) {
+ TimiException.requiredTrue(item.getDeliveredQuantity().compareTo(item.getTotalQuantity()) < 0, "已全部履约的套餐不能取消");
+ }
+ packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED);
+ packageEntity.setCancelledAt(Time.now());
+ packageEntity.setCancelOperatorUserId(userLoginService.getRequireLoginUserId());
+ packageEntity.setCancelReason(reason);
+ packageEntity.setRefundStatus(0 < safeLong(packageEntity.getPaidAmount()) ? GaoCustomerPackage.RefundStatus.PENDING : GaoCustomerPackage.RefundStatus.NOT_REQUIRED);
+ mapper.update(packageEntity);
+ return packageEntity;
+ }
+
+ private GaoEventRecord eventRecordWithAttachments(GaoEventRecord record, GaoCustomerPackagePurchaseRequest request) {
+ record.setAttachmentList(request.getAttachmentList());
+ return record;
+ }
+
+ private GaoCustomer validateCustomer(String customerId, String storeId) {
+ GaoCustomer customer = customerService.get(customerId);
+ TimiException.required(customer, "not found customer");
+ TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户和套餐不属于同一门店");
+ return customer;
+ }
+
+ private GaoEvent validatePurchaseEvent(String eventId) {
+ GaoEvent event = eventService.get(eventId);
+ TimiException.required(event, "not found purchase event");
+ TimiException.requiredTrue(event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE, "事件不是套餐购买事件");
+ TimiException.required(event.getPackageConfigId(), "购买事件未绑定套餐配置");
+ return event;
+ }
+
+ private List defaultPurchaseItems(List configItemList) {
+ return configItemList.stream().map(configItem -> {
+ GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
+ item.setProductName(configItem.getProductName());
+ item.setUnit(configItem.getUnit());
+ item.setTotalQuantity(configItem.getDefaultQuantity());
+ item.setSort(configItem.getSort());
+ item.setRemark(configItem.getRemark());
+ return item;
+ }).toList();
+ }
+
+ private void verifyPurchaseItems(List itemList) {
+ TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品");
+ Set keySet = new HashSet<>();
+ for (int i = 0; i < itemList.size(); i++) {
+ GaoCustomerPackagePurchaseItem item = itemList.get(i);
+ TimiException.required(item, "购买产品明细不能为空");
+ TimiException.required(item.getProductName(), "购买产品名称不能为空");
+ TimiException.required(item.getUnit(), "购买产品单位不能为空");
+ TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空");
+ TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0");
+ TimiException.requiredTrue(3 >= item.getTotalQuantity().scale(), "购买产品数量最多保留三位小数");
+ String key = item.getProductName().trim() + "|" + item.getUnit().trim();
+ TimiException.requiredTrue(keySet.add(key), "购买套餐内产品名称和单位不能重复");
+ item.setProductName(item.getProductName().trim());
+ item.setUnit(item.getUnit().trim());
+ item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
+ }
+ }
+
+ private void verifySameStructureAsConfig(List itemList, List configItemList) {
+ TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致");
+ Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity()));
+ for (GaoPackageConfigItem configItem : configItemList) {
+ GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit());
+ TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致");
+ TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
+ && sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改");
+ }
+ }
+
+ private void verifySameQuantityAsConfig(List itemList, List configItemList) {
+ Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity()));
+ for (GaoPackageConfigItem configItem : configItemList) {
+ GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit());
+ TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额");
+ }
+ }
+
+ private void verifyPurchaseIdempotency(GaoCustomerPackage existing, GaoCustomerPackagePurchaseRequest request) {
+ TimiException.requiredTrue(existing.getCustomerId().equals(request.getCustomerId())
+ && existing.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用");
+ GaoEventRecord eventRecord = eventRecordService.get(existing.getPurchaseEventRecordId());
+ TimiException.required(eventRecord, "购买事件登记不存在");
+ TimiException.requiredTrue(eventRecord.getEventId().equals(request.getEventId())
+ && (request.getPurchasedAt() == null || eventRecord.getRegisteredAt().equals(request.getPurchasedAt()))
+ && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致");
+ List existingItemList = itemMapper.selectByCustomerPackageId(existing.getId());
+ List requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of());
+ verifyPurchaseItems(requestItemList);
+ TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致");
+ Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductNameSnapshot() + "|" + item.getUnitSnapshot(), Function.identity()));
+ for (GaoCustomerPackagePurchaseItem item : requestItemList) {
+ GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductName() + "|" + item.getUnit());
+ TimiException.required(existingItem, "requestNo 对应购买请求内容不一致");
+ TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0
+ && existingItem.getSort().equals(item.getSort())
+ && sameText(existingItem.getRemark(), item.getRemark()), "requestNo 对应购买请求内容不一致");
+ }
+ }
+
+ @Override
+ public Map> mapItemListByCustomerPackageIdList(Collection customerPackageIdList) {
+ if (customerPackageIdList == null || customerPackageIdList.isEmpty()) {
+ return Map.of();
+ }
+ return itemMapper.selectByCustomerPackageIdList(customerPackageIdList).stream()
+ .collect(Collectors.groupingBy(GaoCustomerPackageItem::getCustomerPackageId));
+ }
+
+ private Long pendingAmount(GaoCustomerPackage packageEntity) {
+ return safeLong(packageEntity.getPendingPaidAmount());
+ }
+
+ private long safeLong(Long value) {
+ return value == null ? 0L : value;
+ }
+
+ private String requiredReason(String reason, String message) {
+ TimiException.required(reason, message);
+ String result = reason.trim();
+ TimiException.requiredTrue(!result.isEmpty(), message);
+ return result;
+ }
+
+ private boolean sameText(String left, String right) {
+ return left == null ? right == null : left.equals(right);
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java
index 478f8d2..043cb7a 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java
@@ -139,7 +139,8 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList());
verifyRecordValue(event, record.getNumberValue());
@@ -201,6 +224,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = null;
if (record.getSelectedOptionGroupList() != null) {
eventService.attachOptionGroupList(List.of(event));
@@ -293,6 +319,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService mapper() {
return mapper;
}
+ @Override
+ public PageResult pageWithoutDeleted(Page page) {
+ PageResult result = new PageResult<>();
+ result.setTotal(mapper.countPageWithoutDeleted(page));
+ result.setList(mapper.selectPageWithoutDeleted(page));
+ return result;
+ }
+
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoEvent event) {
@@ -73,6 +86,7 @@ public class GaoEventServiceImplement extends AbstractEntityService implements GaoFundRecordService {
+
+ private final GaoFundRecordMapper mapper;
+ private final GaoCustomerPackageService customerPackageService;
+ private final GaoPackageDeliveryService deliveryService;
+ private final AttachmentService attachmentService;
+ private final GaoStoreScopeService storeScopeService;
+ private final UserLoginService userLoginService;
+ private final LoggerService loggerService;
+
+ @Override
+ protected BaseMapper mapper() {
+ return mapper;
+ }
+
+ /// 资金流水必须通过专用操作创建
+ ///
+ /// @param record 资金流水
+ @Override
+ public void create(GaoFundRecord record) {
+ TimiException.requiredTrue(false, "资金流水必须通过专用接口创建");
+ }
+
+ /// 资金流水不允许通用修改
+ ///
+ /// @param record 资金流水
+ @Override
+ public void update(GaoFundRecord record) {
+ TimiException.requiredTrue(false, "资金流水不允许通用修改");
+ }
+
+ /// 资金流水不允许删除
+ ///
+ /// @param id 资金流水 ID
+ @Override
+ public void delete(String id) {
+ TimiException.requiredTrue(false, "资金流水不允许删除");
+ }
+
+ /// 资金流水不允许物理删除
+ ///
+ /// @param id 资金流水 ID
+ @Override
+ public void destroy(String id) {
+ TimiException.requiredTrue(false, "资金流水不允许物理删除");
+ }
+
+ @Override
+ public PageResult pageByQuery(GaoFundRecordPage page) {
+ TimiException.required(page, "not found page");
+ PageResult result = new PageResult<>();
+ result.setTotal(mapper.countByQuery(page));
+ result.setList(mapper.selectByQuery(page));
+ return result;
+ }
+
+ @Override
+ public List listByCustomerPackageId(String customerPackageId) {
+ TimiException.required(customerPackageId, "not found customerPackageId");
+ return mapper.selectByBizId(customerPackageId);
+ }
+
+ @Override
+ public long countPendingReceiveByBizId(String bizId) {
+ return mapper.countPendingReceiveByBizId(bizId);
+ }
+
+ @Override
+ public GaoFundRecord get(String id) {
+ return super.get(id);
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoFundRecord receive(GaoFundReceiveRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CREATE", request, () -> receiveInternal(request));
+ }
+
+ private GaoFundRecord receiveInternal(GaoFundReceiveRequest request) {
+ TimiException.required(request, "not found receive request");
+ TimiException.required(request.getRequestNo(), "not found requestNo");
+ request.setRequestNo(request.getRequestNo().trim());
+ TimiException.required(request.getCustomerPackageId(), "not found customerPackageId");
+ TimiException.required(request.getAmount(), "not found amount");
+ TimiException.requiredTrue(0 < request.getAmount(), "收款金额必须大于 0");
+ GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId());
+ GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo());
+ if (existing != null) {
+ TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId())
+ && existing.getOperationType() == GaoFundRecord.OperationType.RECEIVE
+ && existing.getAmount().equals(request.getAmount())
+ && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt()))
+ && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用");
+ return get(existing.getId());
+ }
+ TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "收款必须上传照片");
+ checkPackageReceivable(packageEntity);
+ TimiException.requiredTrue(safeLong(packageEntity.getPaidAmount()) + safeLong(packageEntity.getPendingPaidAmount()) + request.getAmount() <= safeLong(packageEntity.getSaleAmount()), "累计收款不能超过成交价");
+ GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.RECEIVE, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark());
+ record.setStatus(GaoFundRecord.Status.PENDING);
+ super.create(record);
+ TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), request.getAmount()), "待核销收款更新失败");
+ attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList());
+ return record;
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoFundRecord verify(String fundRecordId) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_VERIFY", fundRecordId, () -> verifyInternal(fundRecordId));
+ }
+
+ private GaoFundRecord verifyInternal(String fundRecordId) {
+ GaoFundRecord record = mapper.selectForUpdate(fundRecordId);
+ TimiException.required(record, "not found fund record");
+ storeScopeService.checkStoreId(record.getStoreId());
+ TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态");
+ GaoCustomerPackage packageEntity = lockPackage(record.getBizId());
+ TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败");
+ TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), record.getAmount()), "有效收款更新失败");
+ String userId = userLoginService.getRequireLoginUserId();
+ TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化");
+ updatePaymentStatus(packageEntity, safeLong(packageEntity.getPaidAmount()) + record.getAmount(), safeLong(packageEntity.getPendingPaidAmount()) - record.getAmount());
+ record.setStatus(GaoFundRecord.Status.VERIFIED);
+ record.setVerifierUserId(userId);
+ record.setVerifiedAt(Time.now());
+ return record;
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoFundRecord reject(GaoFundActionRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_REJECT", request, () -> releasePending(request, GaoFundRecord.Status.REJECTED));
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoFundRecord cancel(GaoFundActionRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CANCEL", request, () -> {
+ TimiException.required(request, "not found fund action request");
+ TimiException.required(request.getFundRecordId(), "not found fundRecordId");
+ GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId());
+ TimiException.required(record, "not found fund record");
+ TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(record.getOperatorUserId()), "只能撤销自己提交的收款");
+ return releasePending(request, GaoFundRecord.Status.CANCELLED);
+ });
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoFundRecord refund(GaoFundRefundRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REFUND", request, () -> refundInternal(request));
+ }
+
+ private GaoFundRecord refundInternal(GaoFundRefundRequest request) {
+ TimiException.required(request, "not found refund request");
+ TimiException.required(request.getRequestNo(), "not found requestNo");
+ request.setRequestNo(request.getRequestNo().trim());
+ TimiException.required(request.getCustomerPackageId(), "not found customerPackageId");
+ TimiException.required(request.getAmount(), "not found amount");
+ TimiException.requiredTrue(0 < request.getAmount(), "退款金额必须大于 0");
+ TimiException.requiredTrue(hasReason(request.getReason()), "退款原因不能为空");
+ GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId());
+ GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo());
+ if (existing != null) {
+ TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId())
+ && existing.getOperationType() == GaoFundRecord.OperationType.REFUND
+ && existing.getAmount().equals(request.getAmount())
+ && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt()))
+ && sameText(existing.getReason(), request.getReason().trim())
+ && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用");
+ return get(existing.getId());
+ }
+ TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "退款必须上传照片");
+ checkRefundable(packageEntity);
+ TimiException.requiredTrue(request.getAmount() <= safeLong(packageEntity.getPaidAmount()), "退款金额不能超过累计有效收款");
+ GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.REFUND, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark());
+ record.setStatus(GaoFundRecord.Status.VERIFIED);
+ record.setReason(request.getReason().trim());
+ super.create(record);
+ packageEntity.setRefundedAmount(request.getAmount());
+ packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED);
+ customerPackageService.updateAggregateState(packageEntity);
+ attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList());
+ return record;
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_FUND_CONFIRM_NO_REFUND", request, () -> confirmNoRefundInternal(request));
+ }
+
+ private GaoFundRecord confirmNoRefundInternal(GaoFundConfirmNoRefundRequest request) {
+ TimiException.required(request, "not found no refund request");
+ TimiException.required(request.getRequestNo(), "not found requestNo");
+ request.setRequestNo(request.getRequestNo().trim());
+ TimiException.required(request.getCustomerPackageId(), "not found customerPackageId");
+ TimiException.requiredTrue(hasReason(request.getReason()), "确认无需退款原因不能为空");
+ GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId());
+ GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo());
+ if (existing != null) {
+ TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId())
+ && existing.getOperationType() == GaoFundRecord.OperationType.CONFIRM_NO_REFUND
+ && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt()))
+ && sameText(existing.getReason(), request.getReason().trim())
+ && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用");
+ return get(existing.getId());
+ }
+ checkRefundable(packageEntity);
+ GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.CONFIRM_NO_REFUND, request.getRequestNo(), 0L, request.getOccurredAt(), request.getRemark());
+ record.setStatus(GaoFundRecord.Status.VERIFIED);
+ record.setReason(request.getReason().trim());
+ super.create(record);
+ packageEntity.setRefundedAmount(0L);
+ packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED);
+ customerPackageService.updateAggregateState(packageEntity);
+ return record;
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoFundRecord revoke(GaoFundRevokeRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REVOKE", request, () -> revokeInternal(request));
+ }
+
+ private GaoFundRecord revokeInternal(GaoFundRevokeRequest request) {
+ TimiException.required(request, "not found fund revoke request");
+ TimiException.required(request.getRequestNo(), "not found requestNo");
+ request.setRequestNo(request.getRequestNo().trim());
+ TimiException.required(request.getFundRecordId(), "not found fundRecordId");
+ TimiException.requiredTrue(hasReason(request.getReason()), "资金冲正原因不能为空");
+ GaoFundRecord source = mapper.selectForUpdate(request.getFundRecordId());
+ TimiException.required(source, "not found fund record");
+ storeScopeService.checkStoreId(source.getStoreId());
+ GaoFundRecord existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo());
+ if (existing != null) {
+ TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE
+ && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用");
+ return get(existing.getId());
+ }
+ TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "资金冲正必须上传照片");
+ TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正");
+ TimiException.requiredNull(mapper.selectRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正");
+ GaoCustomerPackage packageEntity = lockPackage(source.getBizId());
+ if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) {
+ TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "退款完成后不能冲正相关收款");
+ long paidAfter = safeLong(packageEntity.getPaidAmount()) - source.getAmount();
+ if (0 < deliveryService.countVerifiedByCustomerPackageId(packageEntity.getId())) {
+ TimiException.requiredTrue(packageEntity.getSaleAmount() <= paidAfter, "存在已核销发放,冲正收款后套餐仍须付清");
+ }
+ TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), -source.getAmount()), "有效收款冲正失败");
+ packageEntity.setPaidAmount(paidAfter);
+ updatePaymentStatus(packageEntity, paidAfter, packageEntity.getPendingPaidAmount());
+ } else {
+ TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.REFUNDED, "资金记录退款状态无效");
+ packageEntity.setRefundedAmount(0L);
+ packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.PENDING);
+ customerPackageService.updateAggregateState(packageEntity);
+ }
+ GaoFundRecord revoke = newRecord(packageEntity, GaoFundRecord.OperationType.REVOKE, request.getRequestNo(), source.getAmount(), request.getOccurredAt(), request.getRemark());
+ revoke.setStatus(GaoFundRecord.Status.VERIFIED);
+ revoke.setSourceRecordId(source.getId());
+ revoke.setReason(request.getReason().trim());
+ super.create(revoke);
+ attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList());
+ return revoke;
+ }
+
+ private GaoFundRecord releasePending(GaoFundActionRequest request, GaoFundRecord.Status targetStatus) {
+ TimiException.required(request, "not found fund action request");
+ TimiException.required(request.getFundRecordId(), "not found fundRecordId");
+ TimiException.requiredTrue(hasReason(request.getReason()), "拒绝或撤销原因不能为空");
+ GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId());
+ TimiException.required(record, "not found fund record");
+ storeScopeService.checkStoreId(record.getStoreId());
+ TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态");
+ GaoCustomerPackage packageEntity = lockPackage(record.getBizId());
+ TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败");
+ String userId = userLoginService.getRequireLoginUserId();
+ TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, targetStatus, userId, Time.now(), request.getReason().trim()), "收款记录状态已变化");
+ record.setStatus(targetStatus);
+ record.setVerifierUserId(userId);
+ record.setVerifiedAt(Time.now());
+ record.setReason(request.getReason().trim());
+ return record;
+ }
+
+ private GaoCustomerPackage lockPackage(String id) {
+ GaoCustomerPackage result = customerPackageService.getForUpdate(id);
+ TimiException.required(result, "not found customer package");
+ storeScopeService.checkStoreId(result.getStoreId());
+ return result;
+ }
+
+ private void checkPackageReceivable(GaoCustomerPackage packageEntity) {
+ TimiException.requiredTrue(packageEntity.getStatus() != GaoCustomerPackage.Status.CANCELLED, "已取消套餐不能收款");
+ TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "已完成退款套餐不能收款");
+ }
+
+ private void checkRefundable(GaoCustomerPackage packageEntity) {
+ TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.CANCELLED, "只有已取消套餐可以退款");
+ TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.PENDING, "当前套餐不在待退款状态");
+ }
+
+ private GaoFundRecord newRecord(GaoCustomerPackage packageEntity, GaoFundRecord.OperationType operationType, String requestNo, Long amount, Long occurredAt, String remark) {
+ GaoFundRecord record = new GaoFundRecord();
+ record.setStoreId(packageEntity.getStoreId());
+ record.setCustomerId(packageEntity.getCustomerId());
+ record.setBizType(GaoFundRecord.BizType.CUSTOMER_PACKAGE);
+ record.setBizId(packageEntity.getId());
+ record.setOperationType(operationType);
+ record.setAmount(amount);
+ record.setRequestNo(requestNo);
+ record.setOperatorUserId(userLoginService.getRequireLoginUserId());
+ record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now()));
+ record.setRemark(remark);
+ return record;
+ }
+
+ private void updatePaymentStatus(GaoCustomerPackage packageEntity, long paidAmount, long pendingAmount) {
+ packageEntity.setPaidAmount(paidAmount);
+ packageEntity.setPendingPaidAmount(pendingAmount);
+ packageEntity.setPaymentStatus(safeLong(packageEntity.getSaleAmount()) == 0 || paidAmount >= safeLong(packageEntity.getSaleAmount())
+ ? GaoCustomerPackage.PaymentStatus.PAID
+ : (0 < paidAmount ? GaoCustomerPackage.PaymentStatus.PARTIALLY_PAID : GaoCustomerPackage.PaymentStatus.UNPAID));
+ customerPackageService.updateAggregateState(packageEntity);
+ }
+
+ private boolean hasAttachment(List attachmentList) {
+ for (Attachment attachment : TimiJava.safeIterable(attachmentList)) {
+ if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasReason(String reason) {
+ return reason != null && !reason.trim().isEmpty();
+ }
+
+ private boolean sameText(String left, String right) {
+ return left == null ? right == null : left.equals(right);
+ }
+
+ private long safeLong(Long value) {
+ return value == null ? 0L : value;
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java
new file mode 100644
index 0000000..a36f0f6
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java
@@ -0,0 +1,215 @@
+package com.imyeyu.api.modules.gao.service.implement;
+
+import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
+import com.imyeyu.api.modules.system.entity.Logger;
+import com.imyeyu.api.modules.system.service.LoggerService;
+import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper;
+import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper;
+import com.imyeyu.api.modules.gao.mapper.GaoEventMapper;
+import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
+import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
+import com.imyeyu.api.modules.user.service.UserLoginService;
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.spring.mapper.BaseMapper;
+import com.imyeyu.spring.service.AbstractEntityService;
+import com.imyeyu.utils.Time;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/// GAO 套餐配置服务实现
+///
+/// @author Codex
+/// @since 2026-08-31
+@Service
+@RequiredArgsConstructor
+public class GaoPackageConfigServiceImplement extends AbstractEntityService implements GaoPackageConfigService {
+
+ private final GaoPackageConfigMapper mapper;
+ private final GaoPackageConfigItemMapper itemMapper;
+ private final GaoEventMapper eventMapper;
+ private final UserLoginService userLoginService;
+ private final LoggerService loggerService;
+
+ @Override
+ protected BaseMapper mapper() {
+ return mapper;
+ }
+
+ @Override
+ public PageResult pageByQuery(GaoPackageConfigPage page) {
+ TimiException.required(page, "not found page");
+ PageResult result = new PageResult<>();
+ result.setTotal(mapper.countByQuery(page));
+ result.setList(mapper.selectByQuery(page));
+ return result;
+ }
+
+ @Override
+ public List listValid(String storeId) {
+ TimiException.required(storeId, "not found storeId");
+ return mapper.selectValidList(storeId, Time.now());
+ }
+
+ @Override
+ public Map mapByIdList(Collection idList) {
+ if (idList == null || idList.isEmpty()) {
+ return Map.of();
+ }
+ return mapper.selectByIdList(idList).stream()
+ .collect(java.util.stream.Collectors.toMap(GaoPackageConfig::getId, item -> item));
+ }
+
+ @Override
+ public List listItemByPackageConfigId(String packageConfigId) {
+ TimiException.required(packageConfigId, "not found packageConfigId");
+ return itemMapper.selectByPackageConfigId(packageConfigId);
+ }
+
+ @Override
+ public Map> mapItemListByPackageConfigIdList(Collection packageConfigIdList) {
+ if (packageConfigIdList == null || packageConfigIdList.isEmpty()) {
+ return Map.of();
+ }
+ return itemMapper.selectByPackageConfigIdList(packageConfigIdList).stream()
+ .collect(java.util.stream.Collectors.groupingBy(GaoPackageConfigItem::getPackageConfigId));
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public void create(GaoPackageConfig config) {
+ loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_CREATE", config, () -> {
+ createInternal(config);
+ return config;
+ });
+ }
+
+ private void createInternal(GaoPackageConfig config) {
+ TimiException.required(config, "not found package config");
+ TimiException.required(config.getStoreId(), "not found packageConfig.storeId");
+ TimiException.required(config.getItemList(), "套餐至少配置一个产品");
+ prepareConfig(config, null);
+ config.setUpdatedBy(userLoginService.getRequireLoginUserId());
+ super.create(config);
+ saveItems(config.getId(), config.getItemList());
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public void update(GaoPackageConfig request) {
+ loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_UPDATE", request, () -> {
+ updateInternal(request);
+ return request;
+ });
+ }
+
+ private void updateInternal(GaoPackageConfig request) {
+ TimiException.required(request, "not found package config");
+ TimiException.required(request.getId(), "not found packageConfig.id");
+ GaoPackageConfig dbConfig = get(request.getId());
+ TimiException.required(dbConfig, "not found package config");
+ TimiException.requiredTrue(request.getStoreId() == null || request.getStoreId().equals(dbConfig.getStoreId()), "packageConfig.storeId invalid");
+ dbConfig.setName(request.getName());
+ dbConfig.setDescription(request.getDescription());
+ dbConfig.setStandardAmount(TimiJava.defaultIfNull(request.getStandardAmount(), dbConfig.getStandardAmount()));
+ dbConfig.setStatus(TimiJava.defaultIfNull(request.getStatus(), dbConfig.getStatus()));
+ dbConfig.setBeginAt(request.getBeginAt());
+ dbConfig.setEndAt(request.getEndAt());
+ dbConfig.setSort(request.getSort());
+ prepareConfig(dbConfig, dbConfig.getId());
+ dbConfig.setUpdatedBy(userLoginService.getRequireLoginUserId());
+ super.update(dbConfig);
+ if (request.getItemList() != null) {
+ verifyItems(request.getItemList());
+ GaoPackageConfigItem example = new GaoPackageConfigItem();
+ example.setPackageConfigId(dbConfig.getId());
+ itemMapper.deleteAllByExample(example);
+ saveItems(dbConfig.getId(), request.getItemList());
+ }
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public void delete(String id) {
+ loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_DELETE", id, () -> {
+ GaoPackageConfig config = get(id);
+ TimiException.required(config, "not found package config");
+ TimiException.requiredTrue(0 == eventMapper.countByPackageConfigId(id), "套餐配置已绑定购买事件,不能删除");
+ super.delete(id);
+ return id;
+ });
+ }
+
+ /// 套餐配置只允许逻辑删除
+ ///
+ /// @param id 套餐配置 ID
+ @Override
+ public void destroy(String id) {
+ TimiException.requiredTrue(false, "套餐配置不允许物理删除");
+ }
+
+ private void prepareConfig(GaoPackageConfig config, String excludeId) {
+ String name = trimRequired(config.getName(), "套餐名称不能为空");
+ TimiException.requiredNull(mapper.selectByStoreIdAndName(config.getStoreId(), name, excludeId), "门店内套餐名称不能重复");
+ config.setName(name);
+ config.setStandardAmount(TimiJava.defaultIfNull(config.getStandardAmount(), 0L));
+ TimiException.requiredTrue(0 <= config.getStandardAmount(), "套餐标准价不能为负数");
+ config.setStatus(TimiJava.defaultIfNull(config.getStatus(), GaoPackageConfig.Status.DRAFT));
+ config.setSort(TimiJava.defaultIfNull(config.getSort(), 0));
+ TimiException.requiredTrue(config.getBeginAt() == null || config.getEndAt() == null || config.getBeginAt() <= config.getEndAt(), "套餐有效期无效");
+ if (config.getItemList() != null) {
+ verifyItems(config.getItemList());
+ }
+ }
+
+ private void verifyItems(List itemList) {
+ TimiException.requiredTrue(!itemList.isEmpty(), "套餐至少配置一个产品");
+ Set keySet = new HashSet<>();
+ for (int i = 0; i < itemList.size(); i++) {
+ GaoPackageConfigItem item = itemList.get(i);
+ TimiException.required(item, "套餐产品明细不能为空");
+ String name = trimRequired(item.getProductName(), "产品名称不能为空");
+ String unit = trimRequired(item.getUnit(), "产品单位不能为空");
+ BigDecimal quantity = item.getDefaultQuantity();
+ TimiException.required(quantity, "产品默认数量不能为空");
+ TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0");
+ TimiException.requiredTrue(3 >= quantity.scale(), "产品数量最多保留三位小数");
+ String key = name + "|" + unit;
+ TimiException.requiredTrue(keySet.add(key), "同一套餐内产品名称和单位不能重复");
+ item.setProductName(name);
+ item.setUnit(unit);
+ item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
+ }
+ }
+
+ private void saveItems(String configId, List itemList) {
+ for (GaoPackageConfigItem source : TimiJava.safeIterable(itemList)) {
+ GaoPackageConfigItem item = new GaoPackageConfigItem();
+ item.setPackageConfigId(configId);
+ item.setProductName(source.getProductName());
+ item.setUnit(source.getUnit());
+ item.setDefaultQuantity(source.getDefaultQuantity());
+ item.setSort(source.getSort());
+ item.setRemark(source.getRemark());
+ itemMapper.insert(item);
+ }
+ }
+
+ private String trimRequired(String value, String message) {
+ TimiException.required(value, message);
+ String result = value.trim();
+ TimiException.requiredTrue(!result.isEmpty(), message);
+ return result;
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageDeliveryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageDeliveryServiceImplement.java
new file mode 100644
index 0000000..aed6420
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageDeliveryServiceImplement.java
@@ -0,0 +1,410 @@
+package com.imyeyu.api.modules.gao.service.implement;
+
+import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.common.service.AttachmentService;
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
+import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
+import com.imyeyu.api.modules.gao.mapper.GaoPackageDeliveryItemMapper;
+import com.imyeyu.api.modules.gao.mapper.GaoPackageDeliveryMapper;
+import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
+import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService;
+import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage;
+import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest;
+import com.imyeyu.api.modules.system.entity.Logger;
+import com.imyeyu.api.modules.system.service.LoggerService;
+import com.imyeyu.api.modules.user.service.UserLoginService;
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiCode;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.spring.mapper.BaseMapper;
+import com.imyeyu.spring.service.AbstractEntityService;
+import com.imyeyu.utils.Time;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/// GAO 套餐发放服务实现
+///
+/// @author Codex
+/// @since 2026-08-31
+@Service
+@RequiredArgsConstructor
+public class GaoPackageDeliveryServiceImplement extends AbstractEntityService implements GaoPackageDeliveryService {
+
+ private static final BigDecimal ZERO = BigDecimal.ZERO;
+
+ private final GaoPackageDeliveryMapper mapper;
+ private final GaoPackageDeliveryItemMapper itemMapper;
+ private final GaoCustomerPackageService customerPackageService;
+ private final AttachmentService attachmentService;
+ private final GaoStoreScopeService storeScopeService;
+ private final UserLoginService userLoginService;
+ private final LoggerService loggerService;
+
+ @Override
+ protected BaseMapper mapper() {
+ return mapper;
+ }
+
+ /// 发放流水必须通过专用操作创建
+ ///
+ /// @param delivery 发放流水
+ @Override
+ public void create(GaoPackageDelivery delivery) {
+ throw new TimiException(TimiCode.ERROR, "发放流水必须通过专用接口创建");
+ }
+
+ /// 发放流水不允许通用修改
+ ///
+ /// @param delivery 发放流水
+ @Override
+ public void update(GaoPackageDelivery delivery) {
+ throw new TimiException(TimiCode.ERROR, "发放流水不允许通用修改");
+ }
+
+ /// 发放流水不允许删除
+ ///
+ /// @param id 发放流水 ID
+ @Override
+ public void delete(String id) {
+ throw new TimiException(TimiCode.ERROR, "发放流水不允许删除");
+ }
+
+ /// 发放流水不允许物理删除
+ ///
+ /// @param id 发放流水 ID
+ @Override
+ public void destroy(String id) {
+ throw new TimiException(TimiCode.ERROR, "发放流水不允许物理删除");
+ }
+
+ @Override
+ public PageResult pageByQuery(GaoPackageDeliveryPage page) {
+ TimiException.required(page, "not found page");
+ PageResult result = new PageResult<>();
+ result.setTotal(mapper.countByQuery(page));
+ result.setList(mapper.selectByQuery(page));
+ return result;
+ }
+
+ @Override
+ public List listByCustomerPackageId(String customerPackageId) {
+ TimiException.required(customerPackageId, "not found customerPackageId");
+ return mapper.selectByCustomerPackageId(customerPackageId);
+ }
+
+ @Override
+ public Map> mapItemListByDeliveryIdList(Collection deliveryIdList) {
+ if (deliveryIdList == null || deliveryIdList.isEmpty()) {
+ return Map.of();
+ }
+ return itemMapper.selectByDeliveryIdList(deliveryIdList).stream()
+ .collect(java.util.stream.Collectors.groupingBy(GaoPackageDeliveryItem::getDeliveryId));
+ }
+
+ @Override
+ public long countPendingByCustomerPackageId(String customerPackageId) {
+ return mapper.countPendingByCustomerPackageId(customerPackageId);
+ }
+
+ @Override
+ public long countVerifiedByCustomerPackageId(String customerPackageId) {
+ return mapper.countVerifiedByCustomerPackageId(customerPackageId);
+ }
+
+ @Override
+ public GaoPackageDelivery get(String id) {
+ return super.get(id);
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CREATE", request, () -> createDeliveryInternal(request));
+ }
+
+ private GaoPackageDelivery createDeliveryInternal(GaoPackageDeliveryCreateRequest request) {
+ TimiException.required(request, "not found delivery request");
+ TimiException.required(request.getRequestNo(), "not found requestNo");
+ request.setRequestNo(request.getRequestNo().trim());
+ TimiException.required(request.getCustomerPackageId(), "not found customerPackageId");
+ GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(request.getCustomerPackageId());
+ TimiException.required(packageEntity, "not found customer package");
+ storeScopeService.checkStoreId(packageEntity.getStoreId());
+ GaoPackageDelivery existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo());
+ if (existing != null) {
+ TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "requestNo 已被其他发放操作使用");
+ verifyCreateIdempotency(existing, request);
+ return get(existing.getId());
+ }
+ TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放必须上传照片");
+ TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可发放");
+ TimiException.requiredTrue(packageEntity.getPaymentStatus() == GaoCustomerPackage.PaymentStatus.PAID, "套餐未付清,不能发放");
+ List requestItemList = validateRequestItems(request.getItemList());
+ Map packageItemMap = new HashMap<>();
+ for (GaoPackageDeliveryItem requestItem : requestItemList) {
+ GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(requestItem.getCustomerPackageItemId());
+ TimiException.required(packageItem, "not found customer package item");
+ TimiException.requiredTrue(packageEntity.getId().equals(packageItem.getCustomerPackageId()), "发放产品不属于当前套餐");
+ packageItemMap.put(packageItem.getId(), packageItem);
+ }
+
+ GaoPackageDelivery delivery = new GaoPackageDelivery();
+ delivery.setCustomerPackageId(packageEntity.getId());
+ delivery.setStoreId(packageEntity.getStoreId());
+ delivery.setCustomerId(packageEntity.getCustomerId());
+ delivery.setOperationType(GaoPackageDelivery.OperationType.DELIVER);
+ delivery.setStatus(GaoPackageDelivery.Status.PENDING);
+ delivery.setRequestNo(request.getRequestNo());
+ delivery.setOperatorUserId(userLoginService.getRequireLoginUserId());
+ delivery.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()));
+ delivery.setRemark(request.getRemark());
+ super.create(delivery);
+ for (GaoPackageDeliveryItem requestItem : requestItemList) {
+ GaoCustomerPackageItem packageItem = packageItemMap.get(requestItem.getCustomerPackageItemId());
+ BigDecimal reservedBefore = decimal(packageItem.getReservedQuantity());
+ BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity());
+ BigDecimal available = decimal(packageItem.getTotalQuantity()).subtract(reservedBefore).subtract(deliveredBefore);
+ TimiException.requiredTrue(0 <= 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, "当前套餐不可核销发放");
+ TimiException.requiredTrue(packageEntity.getPaymentStatus() == GaoCustomerPackage.PaymentStatus.PAID, "套餐未付清,不能核销发放");
+ List itemList = itemMapper.selectByDeliveryId(delivery.getId());
+ for (GaoPackageDeliveryItem deliveryItem : itemList) {
+ GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(deliveryItem.getCustomerPackageItemId());
+ TimiException.required(packageItem, "not found customer package item");
+ TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(packageItem.getId(), deliveryItem.getQuantity().negate()), "发放占用数量异常");
+ TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), deliveryItem.getQuantity()), "发放数量异常");
+ }
+ TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, GaoPackageDelivery.Status.VERIFIED, userLoginService.getRequireLoginUserId(), Time.now(), delivery.getReason()), "发放单状态已变化");
+ delivery.setStatus(GaoPackageDelivery.Status.VERIFIED);
+ delivery.setVerifierUserId(userLoginService.getRequireLoginUserId());
+ delivery.setVerifiedAt(Time.now());
+ refreshPackageStatus(packageEntity);
+ return delivery;
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REJECT", request, () -> releasePending(request, GaoPackageDelivery.Status.REJECTED));
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CANCEL", request, () -> cancelInternal(request));
+ }
+
+ private GaoPackageDelivery cancelInternal(GaoPackageDeliveryActionRequest request) {
+ TimiException.required(request, "not found delivery request");
+ GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId());
+ TimiException.required(delivery, "not found delivery");
+ String loginUserId = userLoginService.getRequireLoginUserId();
+ TimiException.requiredTrue(loginUserId.equals(delivery.getOperatorUserId()), "只能撤销自己提交的发放");
+ return releasePending(request, GaoPackageDelivery.Status.CANCELLED);
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request) {
+ return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REVOKE", request, () -> revokeInternal(request));
+ }
+
+ private GaoPackageDelivery revokeInternal(GaoPackageDeliveryRevokeRequest request) {
+ TimiException.required(request, "not found revoke request");
+ TimiException.required(request.getRequestNo(), "not found requestNo");
+ request.setRequestNo(request.getRequestNo().trim());
+ TimiException.required(request.getDeliveryId(), "not found deliveryId");
+ TimiException.required(hasReason(request.getReason()), "冲正原因不能为空");
+ GaoPackageDelivery source = mapper.selectForUpdate(request.getDeliveryId());
+ TimiException.required(source, "not found delivery");
+ storeScopeService.checkStoreId(source.getStoreId());
+ GaoPackageDelivery existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo());
+ if (existing != null) {
+ TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.REVOKE
+ && source.getId().equals(existing.getSourceDeliveryId()), "requestNo 已被其他发放操作使用");
+ return get(existing.getId());
+ }
+ TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放冲正必须上传照片");
+ TimiException.requiredTrue(source.getOperationType() == GaoPackageDelivery.OperationType.DELIVER && source.getStatus() == GaoPackageDelivery.Status.VERIFIED, "只有已核销发放可以冲正");
+ TimiException.requiredNull(mapper.selectRevokeBySourceDeliveryId(source.getId()), "该发放单已经冲正");
+ GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(source.getCustomerPackageId());
+ TimiException.required(packageEntity, "not found customer package");
+ List sourceItemList = itemMapper.selectByDeliveryId(source.getId());
+ GaoPackageDelivery revoke = new GaoPackageDelivery();
+ revoke.setCustomerPackageId(source.getCustomerPackageId());
+ revoke.setStoreId(source.getStoreId());
+ revoke.setCustomerId(source.getCustomerId());
+ revoke.setOperationType(GaoPackageDelivery.OperationType.REVOKE);
+ revoke.setStatus(GaoPackageDelivery.Status.VERIFIED);
+ revoke.setRequestNo(request.getRequestNo());
+ revoke.setOperatorUserId(userLoginService.getRequireLoginUserId());
+ revoke.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()));
+ revoke.setVerifierUserId(userLoginService.getRequireLoginUserId());
+ revoke.setVerifiedAt(Time.now());
+ revoke.setSourceDeliveryId(source.getId());
+ revoke.setReason(request.getReason().trim());
+ revoke.setRemark(request.getRemark());
+ super.create(revoke);
+ for (GaoPackageDeliveryItem sourceItem : sourceItemList) {
+ GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(sourceItem.getCustomerPackageItemId());
+ TimiException.required(packageItem, "not found customer package item");
+ BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity());
+ TimiException.requiredTrue(0 < deliveredBefore.compareTo(sourceItem.getQuantity()) || deliveredBefore.compareTo(sourceItem.getQuantity()) == 0, "已发数量不足以冲正");
+ TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), sourceItem.getQuantity().negate()), "发放数量冲正失败");
+ GaoPackageDeliveryItem revokeItem = copyDeliveryItem(sourceItem);
+ revokeItem.setDeliveryId(revoke.getId());
+ revokeItem.setReservedBefore(packageItem.getReservedQuantity());
+ revokeItem.setReservedAfter(packageItem.getReservedQuantity());
+ revokeItem.setDeliveredBefore(deliveredBefore);
+ revokeItem.setDeliveredAfter(deliveredBefore.subtract(sourceItem.getQuantity()));
+ itemMapper.insert(revokeItem);
+ }
+ TimiException.requiredTrue(0 < mapper.updateStatusIf(source.getId(), GaoPackageDelivery.Status.VERIFIED, GaoPackageDelivery.Status.REVOKED, source.getVerifierUserId(), source.getVerifiedAt(), source.getReason()), "发放单状态已变化");
+ if (packageEntity.getStatus() == GaoCustomerPackage.Status.FULFILLED) {
+ packageEntity.setStatus(GaoCustomerPackage.Status.ACTIVE);
+ customerPackageService.updateAggregateState(packageEntity);
+ }
+ attachmentService.updateByBizId(Attachment.BizType.GAO_PACKAGE_DELIVERY, revoke.getId(), request.getAttachmentList());
+ return revoke;
+ }
+
+ private GaoPackageDelivery releasePending(GaoPackageDeliveryActionRequest request, GaoPackageDelivery.Status targetStatus) {
+ TimiException.required(request, "not found delivery request");
+ TimiException.required(request.getDeliveryId(), "not found deliveryId");
+ TimiException.required(hasReason(request.getReason()), "拒绝或撤销原因不能为空");
+ GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId());
+ TimiException.required(delivery, "not found delivery");
+ storeScopeService.checkStoreId(delivery.getStoreId());
+ TimiException.requiredTrue(delivery.getStatus() == GaoPackageDelivery.Status.PENDING && delivery.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "发放单不是待核销状态");
+ for (GaoPackageDeliveryItem item : itemMapper.selectByDeliveryId(delivery.getId())) {
+ TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(item.getCustomerPackageItemId(), item.getQuantity().negate()), "发放占用数量异常");
+ }
+ String loginUserId = userLoginService.getRequireLoginUserId();
+ TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, targetStatus, loginUserId, Time.now(), request.getReason().trim()), "发放单状态已变化");
+ delivery.setStatus(targetStatus);
+ delivery.setVerifierUserId(loginUserId);
+ delivery.setVerifiedAt(Time.now());
+ delivery.setReason(request.getReason().trim());
+ return delivery;
+ }
+
+ private void refreshPackageStatus(GaoCustomerPackage packageEntity) {
+ List itemList = customerPackageService.listItemByCustomerPackageId(packageEntity.getId());
+ boolean fulfilled = !itemList.isEmpty();
+ for (GaoCustomerPackageItem item : itemList) {
+ if (decimal(item.getDeliveredQuantity()).compareTo(decimal(item.getTotalQuantity())) < 0) {
+ fulfilled = false;
+ break;
+ }
+ }
+ if (fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) {
+ packageEntity.setStatus(GaoCustomerPackage.Status.FULFILLED);
+ customerPackageService.updateAggregateState(packageEntity);
+ }
+ }
+
+ private List validateRequestItems(List sourceList) {
+ TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "发放至少需要一个产品");
+ Set idSet = new HashSet<>();
+ List result = new ArrayList<>();
+ for (GaoPackageDeliveryItem source : sourceList) {
+ TimiException.required(source, "发放产品明细不能为空");
+ TimiException.required(source.getCustomerPackageItemId(), "not found customerPackageItemId");
+ TimiException.required(source.getQuantity(), "发放数量不能为空");
+ TimiException.requiredTrue(0 < source.getQuantity().signum(), "发放数量必须大于 0");
+ TimiException.requiredTrue(3 >= source.getQuantity().scale(), "发放数量最多保留三位小数");
+ TimiException.requiredTrue(idSet.add(source.getCustomerPackageItemId()), "同一发放单产品不能重复");
+ result.add(source);
+ }
+ result.sort(Comparator.comparing(GaoPackageDeliveryItem::getCustomerPackageItemId));
+ return result;
+ }
+
+ private GaoPackageDeliveryItem copyDeliveryItem(GaoPackageDeliveryItem source) {
+ GaoPackageDeliveryItem result = new GaoPackageDeliveryItem();
+ result.setCustomerPackageItemId(source.getCustomerPackageItemId());
+ result.setQuantity(source.getQuantity());
+ return result;
+ }
+
+ private void verifyCreateIdempotency(GaoPackageDelivery existing, GaoPackageDeliveryCreateRequest request) {
+ TimiException.requiredTrue(existing.getCustomerPackageId().equals(request.getCustomerPackageId())
+ && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
+ && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应请求内容不一致");
+ List existingItems = itemMapper.selectByDeliveryId(existing.getId());
+ List requestItems = validateRequestItems(request.getItemList());
+ TimiException.requiredTrue(existingItems.size() == requestItems.size(), "requestNo 对应请求内容不一致");
+ Map requestMap = requestItems.stream().collect(java.util.stream.Collectors.toMap(GaoPackageDeliveryItem::getCustomerPackageItemId, GaoPackageDeliveryItem::getQuantity));
+ for (GaoPackageDeliveryItem item : existingItems) {
+ TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getCustomerPackageItemId(), ZERO)) == 0, "requestNo 对应请求内容不一致");
+ }
+ }
+
+ private boolean hasAttachment(List attachmentList) {
+ for (Attachment attachment : TimiJava.safeIterable(attachmentList)) {
+ if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasReason(String reason) {
+ return reason != null && !reason.trim().isEmpty();
+ }
+
+ private boolean sameText(String left, String right) {
+ return left == null ? right == null : left.equals(right);
+ }
+
+ private BigDecimal decimal(BigDecimal value) {
+ return value == null ? ZERO : value;
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java
new file mode 100644
index 0000000..9ce6188
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java
@@ -0,0 +1,119 @@
+package com.imyeyu.api.modules.gao.service.implement;
+
+import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
+import com.imyeyu.api.modules.common.entity.Article;
+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.AttachmentService;
+import com.imyeyu.api.modules.gao.service.GaoStoreDiaryService;
+import com.imyeyu.api.modules.gao.mapper.GaoStoreDiaryMapper;
+import com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage;
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.bean.Page;
+import com.imyeyu.spring.bean.PageResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/// GAO 门店日记服务实现
+///
+/// @author Codex
+/// @since 2026-08-31
+@Service
+@RequiredArgsConstructor
+public class GaoStoreDiaryServiceImplement implements GaoStoreDiaryService {
+
+ private final ArticleService articleService;
+ private final AttachmentService attachmentService;
+ private final ArticleMapper articleMapper;
+ private final GaoStoreDiaryMapper diaryMapper;
+
+ @Override
+ public PageResult page(GaoStoreDiaryPage page) {
+ PageResult result = new PageResult<>();
+ result.setTotal(diaryMapper.countByMonth(page));
+ result.setList(diaryMapper.selectByMonth(page));
+ return result;
+ }
+
+ @Override
+ public Article get(String id) {
+ Article article = articleService.get(id);
+ checkStoreDiary(article);
+ return article;
+ }
+
+ @Override
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ public void create(Article article, String userId) {
+ prepare(article, userId);
+ articleService.create(article);
+ }
+
+ @Override
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ public void update(Article article, String userId) {
+ TimiException.required(article, "未找到门店日记");
+ TimiException.required(article.getId(), "未找到门店日记 ID");
+ Article dbArticle = get(article.getId());
+ article.setBizType(dbArticle.getBizType());
+ article.setBizId(dbArticle.getBizId());
+ article.setCreatedAt(null);
+ article.setUpdatedAt(null);
+ article.setDeletedAt(null);
+ article.setReads(dbArticle.getReads());
+ article.setLikes(dbArticle.getLikes());
+ article.setUpdatedBy(userId);
+ articleService.update(article);
+ }
+
+ @Override
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ public void delete(String id) {
+ Article article = get(id);
+ attachmentService.deleteByBizId(Attachment.BizType.ARTICLE, article.getId());
+ articleService.delete(article.getId());
+ }
+
+ @Override
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ public void deleteByStoreId(String storeId) {
+ TimiException.required(storeId, "未找到门店 ID");
+ Article example = new Article();
+ example.setBizType(Article.BizType.GAO_STORE_DIARY);
+ example.setBizId(storeId);
+ for (Article article : articleMapper.selectAllByExample(example)) {
+ attachmentService.deleteByBizId(Attachment.BizType.ARTICLE, article.getId());
+ articleService.delete(article.getId());
+ }
+ }
+
+ /// 固定门店日记业务类型并写入更新用户
+ ///
+ /// @param article 门店日记
+ /// @param userId 操作用户 ID
+ private void prepare(Article article, String userId) {
+ TimiException.required(article, "未找到门店日记");
+ TimiException.required(article.getBizId(), "未找到门店 ID");
+ TimiException.required(userId, "未找到更新用户 ID");
+ article.setBizType(Article.BizType.GAO_STORE_DIARY);
+ article.setCreatedAt(null);
+ article.setUpdatedAt(null);
+ article.setDeletedAt(null);
+ article.setUpdatedBy(userId);
+ article.setReads(0);
+ article.setLikes(0);
+ article.setStatus(TimiJava.defaultIfNull(article.getStatus(), Article.Status.NORMAL));
+ }
+
+ /// 校验文章确实是门店日记
+ ///
+ /// @param article 文章
+ private void checkStoreDiary(Article article) {
+ TimiException.required(article, "未找到门店日记");
+ TimiException.requiredTrue(article.getBizType() == Article.BizType.GAO_STORE_DIARY, "门店日记类型无效");
+ TimiException.required(article.getBizId(), "未找到门店 ID");
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java
index 4358a44..4a1fdad 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java
@@ -11,6 +11,7 @@ import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
+import com.imyeyu.api.modules.gao.service.GaoStoreDiaryService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.service.GaoUserService;
import com.imyeyu.api.modules.gao.util.StoreXSSFBuilder;
@@ -55,6 +56,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService gaoUserServiceProvider;
private final TagService tagService;
@@ -140,6 +142,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService itemList;
+
+ /// 购买照片
+ private List attachmentList;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java
new file mode 100644
index 0000000..4ca1626
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java
@@ -0,0 +1,17 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.Data;
+
+/// GAO 资金收款状态操作请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoFundActionRequest {
+
+ /// 资金记录 ID
+ private String fundRecordId;
+
+ /// 拒绝或撤销原因
+ private String reason;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java
new file mode 100644
index 0000000..786ce1b
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java
@@ -0,0 +1,26 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.Data;
+
+/// GAO 确认无需退款请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoFundConfirmNoRefundRequest {
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 确认时间
+ private Long occurredAt;
+
+ /// 确认原因
+ private String reason;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java
new file mode 100644
index 0000000..aedb2e8
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java
@@ -0,0 +1,32 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import lombok.Data;
+
+import java.util.List;
+
+/// GAO 线下收款登记请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoFundReceiveRequest {
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 收款金额,单位分
+ private Long amount;
+
+ /// 收款发生时间
+ private Long occurredAt;
+
+ /// 收款照片
+ private List attachmentList;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java
new file mode 100644
index 0000000..ec0d9f2
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java
@@ -0,0 +1,36 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
+import com.imyeyu.java.bean.BasePage;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/// GAO 资金流水分页查询请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoFundRecordPage extends BasePage {
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 客户 ID
+ private String customerId;
+
+ /// 操作类型
+ private GaoFundRecord.OperationType operationType;
+
+ /// 操作状态
+ private GaoFundRecord.Status status;
+
+ /// 操作开始时间
+ private Long beginAt;
+
+ /// 操作结束时间
+ private Long endAt;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java
new file mode 100644
index 0000000..81dbb35
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java
@@ -0,0 +1,35 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import lombok.Data;
+
+import java.util.List;
+
+/// GAO 退款登记请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoFundRefundRequest {
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 退款金额,单位分
+ private Long amount;
+
+ /// 退款发生时间
+ private Long occurredAt;
+
+ /// 退款原因
+ private String reason;
+
+ /// 退款照片
+ private List attachmentList;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRevokeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRevokeRequest.java
new file mode 100644
index 0000000..2cdfb8e
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRevokeRequest.java
@@ -0,0 +1,32 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import lombok.Data;
+
+import java.util.List;
+
+/// GAO 资金流水冲正请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoFundRevokeRequest {
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 原资金记录 ID
+ private String fundRecordId;
+
+ /// 冲正发生时间
+ private Long occurredAt;
+
+ /// 冲正原因
+ private String reason;
+
+ /// 冲正照片
+ private List attachmentList;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageConfigPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageConfigPage.java
new file mode 100644
index 0000000..715593a
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageConfigPage.java
@@ -0,0 +1,24 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
+import com.imyeyu.java.bean.BasePage;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/// GAO 套餐配置分页查询请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoPackageConfigPage extends BasePage {
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 套餐名称关键词
+ private String keyword;
+
+ /// 套餐状态
+ private GaoPackageConfig.Status status;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java
new file mode 100644
index 0000000..dfd2e7c
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java
@@ -0,0 +1,17 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.Data;
+
+/// GAO 套餐发放状态操作请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoPackageDeliveryActionRequest {
+
+ /// 发放单 ID
+ private String deliveryId;
+
+ /// 拒绝或撤销原因
+ private String reason;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java
new file mode 100644
index 0000000..4eede73
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java
@@ -0,0 +1,33 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
+import lombok.Data;
+
+import java.util.List;
+
+/// GAO 套餐发放请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoPackageDeliveryCreateRequest {
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 实际发放时间
+ private Long deliveredAt;
+
+ /// 发放产品明细
+ private List itemList;
+
+ /// 发放照片
+ private List attachmentList;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java
new file mode 100644
index 0000000..73c788e
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java
@@ -0,0 +1,36 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
+import com.imyeyu.java.bean.BasePage;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/// GAO 套餐发放分页查询请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoPackageDeliveryPage extends BasePage {
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 客户套餐 ID
+ private String customerPackageId;
+
+ /// 客户 ID
+ private String customerId;
+
+ /// 发放状态
+ private GaoPackageDelivery.Status status;
+
+ /// 操作类型
+ private GaoPackageDelivery.OperationType operationType;
+
+ /// 发放开始时间
+ private Long beginAt;
+
+ /// 发放结束时间
+ private Long endAt;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java
new file mode 100644
index 0000000..ec9e207
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java
@@ -0,0 +1,32 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import lombok.Data;
+
+import java.util.List;
+
+/// GAO 已核销发放整单冲正请求
+///
+/// @author Codex
+/// @since 2026-08-31
+@Data
+public class GaoPackageDeliveryRevokeRequest {
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 原发放单 ID
+ private String deliveryId;
+
+ /// 冲正发生时间
+ private Long deliveredAt;
+
+ /// 冲正原因
+ private String reason;
+
+ /// 冲正照片
+ private List attachmentList;
+
+ /// 备注
+ private String remark;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStoreDiaryPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStoreDiaryPage.java
new file mode 100644
index 0000000..4739549
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStoreDiaryPage.java
@@ -0,0 +1,23 @@
+package com.imyeyu.api.modules.gao.vo;
+
+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 GaoStoreDiaryPage extends BasePage {
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 创建时间起点,包含
+ private Long beginCreatedAt;
+
+ /// 创建时间终点,不包含
+ private Long endCreatedAt;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java b/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java
index 45e4463..39f8afa 100644
--- a/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java
+++ b/src/main/java/com/imyeyu/api/modules/system/service/LoggerService.java
@@ -6,6 +6,8 @@ import com.imyeyu.spring.service.DestroyableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.PageableService;
+import java.util.function.Supplier;
+
/**
* 日志服务
*
@@ -16,6 +18,51 @@ public interface LoggerService extends GettableService, Pageable
void createSync(Logger logger);
+ /// 执行并记录有返回值的业务操作
+ ///
+ /// @param logger 日志对象,调用方负责设置模块、操作和请求内容
+ /// @param action 业务操作
+ /// @param 返回值类型
+ /// @return 业务操作返回值
+ T execute(Logger logger, Supplier action);
+
+ /// 执行并记录无返回值的业务操作
+ ///
+ /// @param logger 日志对象,调用方负责设置模块、操作和请求内容
+ /// @param action 业务操作
+ default void execute(Logger logger, Runnable action) {
+ execute(logger, () -> {
+ action.run();
+ return null;
+ });
+ }
+
+ /// 创建日志并执行有返回值的业务操作
+ ///
+ /// @param module 日志模块
+ /// @param operation 操作描述
+ /// @param content 请求内容
+ /// @param action 业务操作
+ /// @param 返回值类型
+ /// @return 业务操作返回值
+ default T execute(Logger.Module module, String operation, Object content, Supplier action) {
+ Logger logger = new Logger(module, operation);
+ logger.setContent(content == null ? null : content.toString());
+ return execute(logger, action);
+ }
+
+ /// 创建日志并执行无返回值的业务操作
+ ///
+ /// @param module 日志模块
+ /// @param operation 操作描述
+ /// @param content 请求内容
+ /// @param action 业务操作
+ default void execute(Logger.Module module, String operation, Object content, Runnable action) {
+ Logger logger = new Logger(module, operation);
+ logger.setContent(content == null ? null : content.toString());
+ execute(logger, action);
+ }
+
void create(Logger.Module module, Logger.Level level, String operation, String content, String result);
default void info(Logger.Module module, String operation) {
diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java
index af0a835..dff254a 100644
--- a/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java
@@ -6,13 +6,17 @@ import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
+import com.imyeyu.spring.entity.IDEntity;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import java.util.function.Supplier;
+
/**
* 日志服务实现
*
@@ -46,6 +50,32 @@ public class LoggerServiceImplement extends AbstractEntityService T execute(Logger logger, Supplier action) {
+ try {
+ T result = action.get();
+ logger.setLevel(Logger.Level.INFO);
+ if (result instanceof IDEntity> entity) {
+ logger.setResult(String.valueOf(entity.getId()));
+ } else if (result != null) {
+ logger.setResult(result.toString());
+ }
+ create(logger);
+ return result;
+ } catch (TimiException e) {
+ logger.setLevel(Logger.Level.WARN);
+ logger.setException(e.getMessage());
+ create(logger);
+ throw e;
+ } catch (RuntimeException e) {
+ logger.setLevel(Logger.Level.ERROR);
+ logger.setException(TimiJava.serializeThrowable(e));
+ log.error("业务操作失败: {}", logger.getOperation(), e);
+ create(logger);
+ throw e;
+ }
+ }
+
public void create(Logger.Module module, Logger.Level level, String operation, String content, String result) {
Logger log = new Logger(module, level, operation);
log.setContent(content);
diff --git a/src/main/java/db/migration/timiserver/V50__reconcile_attachment_name_length.java b/src/main/java/db/migration/timiserver/V50__reconcile_attachment_name_length.java
new file mode 100644
index 0000000..72dd463
--- /dev/null
+++ b/src/main/java/db/migration/timiserver/V50__reconcile_attachment_name_length.java
@@ -0,0 +1,76 @@
+package db.migration.timiserver;
+
+import org.flywaydb.core.api.migration.BaseJavaMigration;
+import org.flywaydb.core.api.migration.Context;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+/// 对账附件名称字段长度
+///
+/// 生产环境可能已经紧急扩容,达到目标长度时不重复执行 DDL
+public class V50__reconcile_attachment_name_length extends BaseJavaMigration {
+
+ private static final int NAME_LENGTH = 255;
+
+ @Override
+ public void migrate(Context context) throws Exception {
+ Connection connection = context.getConnection();
+ int attachmentNameLength = queryCurrentLength(connection, "attachment");
+ int variantNameLength = queryCurrentLength(connection, "attachment_variant");
+ reconcile(connection, "attachment", attachmentNameLength);
+ reconcile(connection, "attachment_variant", variantNameLength);
+ }
+
+ /// 将名称字段扩容到目标长度
+ ///
+ /// @param connection Flyway 数据库连接
+ /// @param tableName 表名
+ /// @param currentLength 当前字符长度
+ /// @throws SQLException DDL 执行失败
+ private void reconcile(Connection connection, String tableName, int currentLength) throws SQLException {
+ if (NAME_LENGTH <= currentLength) {
+ return;
+ }
+
+ String sql = """
+ ALTER TABLE `%s`
+ MODIFY COLUMN `name` VARCHAR(%d) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL
+ """.formatted(tableName, NAME_LENGTH);
+ try (Statement statement = connection.createStatement()) {
+ statement.executeUpdate(sql);
+ }
+ }
+
+ /// 查询名称字段当前长度
+ ///
+ /// @param connection Flyway 数据库连接
+ /// @param tableName 表名
+ /// @return 当前字符长度
+ /// @throws SQLException 表或字段不存在、字段类型不正确或查询失败
+ private int queryCurrentLength(Connection connection, String tableName) throws SQLException {
+ String sql = """
+ SELECT `DATA_TYPE`, `CHARACTER_MAXIMUM_LENGTH`
+ FROM `INFORMATION_SCHEMA`.`COLUMNS`
+ WHERE `TABLE_SCHEMA` = DATABASE()
+ AND `TABLE_NAME` = ?
+ AND `COLUMN_NAME` = 'name'
+ """;
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ statement.setString(1, tableName);
+ try (ResultSet resultSet = statement.executeQuery()) {
+ if (!resultSet.next()) {
+ throw new SQLException("表 %s 的 name 字段不存在".formatted(tableName));
+ }
+ String dataType = resultSet.getString("DATA_TYPE");
+ if (!"varchar".equalsIgnoreCase(dataType)) {
+ throw new SQLException("表 %s 的 name 字段类型不是 VARCHAR,而是 %s".formatted(tableName, dataType));
+ }
+ return resultSet.getInt("CHARACTER_MAXIMUM_LENGTH");
+ }
+ }
+ }
+}
diff --git a/src/main/resources/db/migration/timiserver/V47__add_article_updated_by.sql b/src/main/resources/db/migration/timiserver/V47__add_article_updated_by.sql
new file mode 100644
index 0000000..8a1d3ac
--- /dev/null
+++ b/src/main/resources/db/migration/timiserver/V47__add_article_updated_by.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `article`
+ ADD COLUMN `updated_by` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '最后更新用户 ID' AFTER `updated_at`;
diff --git a/src/main/resources/db/migration/timiserver/V48__create_gao_package_tables.sql b/src/main/resources/db/migration/timiserver/V48__create_gao_package_tables.sql
new file mode 100644
index 0000000..78474b2
--- /dev/null
+++ b/src/main/resources/db/migration/timiserver/V48__create_gao_package_tables.sql
@@ -0,0 +1,157 @@
+ALTER TABLE `gao_event`
+ ADD COLUMN `biz_type` VARCHAR(32) NOT NULL DEFAULT 'GENERAL' COMMENT '业务类型,GENERAL 普通登记,PACKAGE_PURCHASE 购买产品套餐' AFTER `id`;
+
+CREATE TABLE IF NOT EXISTS `gao_package_config` (
+ `id` VARCHAR(36) NOT NULL,
+ `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID',
+ `name` VARCHAR(128) NOT NULL COMMENT '套餐名称',
+ `description` VARCHAR(255) NULL COMMENT '套餐说明',
+ `standard_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '标准总价,单位分',
+ `status` VARCHAR(32) NOT NULL COMMENT 'DRAFT 草稿,ACTIVE 启用,INACTIVE 停用',
+ `begin_at` BIGINT NULL COMMENT '有效期开始时间戳,毫秒',
+ `end_at` BIGINT NULL COMMENT '有效期结束时间戳,毫秒',
+ `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值',
+ `updated_by` VARCHAR(36) NOT NULL COMMENT '最后修改用户 ID',
+ `created_at` BIGINT NOT NULL,
+ `updated_at` BIGINT NULL,
+ `deleted_at` BIGINT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_gao_package_config_store_status` (`store_id`, `status`, `deleted_at`, `begin_at`, `end_at`, `sort`)
+) COMMENT='GAO 套餐配置';
+
+CREATE TABLE IF NOT EXISTS `gao_package_config_item` (
+ `id` VARCHAR(36) NOT NULL,
+ `package_config_id` VARCHAR(36) NOT NULL COMMENT '套餐配置 ID',
+ `product_name` VARCHAR(128) NOT NULL COMMENT '产品名称',
+ `unit` VARCHAR(32) NOT NULL COMMENT '产品单位',
+ `default_quantity` DECIMAL(18,3) NOT NULL COMMENT '默认数量',
+ `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值',
+ `remark` VARCHAR(255) NULL COMMENT '备注',
+ `created_at` BIGINT NOT NULL,
+ `updated_at` BIGINT NULL,
+ `deleted_at` BIGINT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_gao_package_config_item_config` (`package_config_id`, `deleted_at`, `sort`, `created_at`)
+) COMMENT='GAO 套餐配置产品明细';
+
+CREATE TABLE IF NOT EXISTS `gao_customer_package` (
+ `id` VARCHAR(36) NOT NULL,
+ `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
+ `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID',
+ `package_config_id` VARCHAR(36) NOT NULL COMMENT '来源套餐配置 ID',
+ `purchase_event_record_id` VARCHAR(36) NOT NULL COMMENT '购买事件登记 ID',
+ `request_no` VARCHAR(128) NOT NULL COMMENT '购买幂等请求号',
+ `package_name_snapshot` VARCHAR(128) NOT NULL COMMENT '套餐名称快照',
+ `standard_amount_snapshot` BIGINT NOT NULL COMMENT '标准价快照,单位分',
+ `sale_amount` BIGINT NOT NULL COMMENT '实际成交价,单位分',
+ `pending_paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '待核销收款合计,单位分',
+ `paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '累计有效收款,单位分',
+ `refunded_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '累计有效退款,单位分',
+ `status` VARCHAR(32) NOT NULL COMMENT '套餐状态',
+ `payment_status` VARCHAR(32) NOT NULL COMMENT '付款状态',
+ `refund_status` VARCHAR(32) NOT NULL COMMENT '退款状态',
+ `purchased_at` BIGINT NOT NULL COMMENT '购买时间',
+ `operator_user_id` VARCHAR(36) NOT NULL COMMENT '购买登记人',
+ `cancelled_at` BIGINT NULL COMMENT '取消时间',
+ `cancel_operator_user_id` VARCHAR(36) NULL COMMENT '取消操作人',
+ `cancel_reason` TEXT NULL COMMENT '取消原因',
+ `remark` TEXT NULL COMMENT '备注',
+ `created_at` BIGINT NOT NULL,
+ `updated_at` BIGINT NULL,
+ `deleted_at` BIGINT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_gao_customer_package_purchase_event` (`purchase_event_record_id`),
+ UNIQUE KEY `uk_gao_customer_package_request` (`store_id`, `request_no`),
+ KEY `idx_gao_customer_package_customer` (`store_id`, `customer_id`, `status`, `deleted_at`, `purchased_at`),
+ KEY `idx_gao_customer_package_payment` (`store_id`, `payment_status`, `refund_status`, `deleted_at`, `purchased_at`)
+) COMMENT='GAO 客户套餐';
+
+CREATE TABLE IF NOT EXISTS `gao_customer_package_item` (
+ `id` VARCHAR(36) NOT NULL,
+ `customer_package_id` VARCHAR(36) NOT NULL COMMENT '客户套餐 ID',
+ `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照',
+ `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '产品单位快照',
+ `total_quantity` DECIMAL(18,3) NOT NULL COMMENT '总权益数量',
+ `reserved_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '待核销占用数量',
+ `delivered_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已核销净发放数量',
+ `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值',
+ `remark` VARCHAR(255) NULL COMMENT '备注',
+ `created_at` BIGINT NOT NULL,
+ `updated_at` BIGINT NULL,
+ `deleted_at` BIGINT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_gao_customer_package_item_package` (`customer_package_id`, `deleted_at`, `sort`, `created_at`)
+) COMMENT='GAO 客户套餐产品权益快照';
+
+CREATE TABLE IF NOT EXISTS `gao_package_delivery` (
+ `id` VARCHAR(36) NOT NULL,
+ `customer_package_id` VARCHAR(36) NOT NULL COMMENT '客户套餐 ID',
+ `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
+ `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID',
+ `operation_type` VARCHAR(32) NOT NULL COMMENT 'DELIVER 正常发放,REVOKE 整单冲正',
+ `status` VARCHAR(32) NOT NULL COMMENT '发放状态',
+ `request_no` VARCHAR(128) NOT NULL COMMENT '客户端幂等请求号',
+ `operator_user_id` VARCHAR(36) NOT NULL COMMENT '提交人',
+ `delivered_at` BIGINT NOT NULL COMMENT '实际发放时间',
+ `verifier_user_id` VARCHAR(36) NULL COMMENT '核销或拒绝操作人',
+ `verified_at` BIGINT NULL COMMENT '核销或拒绝时间',
+ `source_delivery_id` VARCHAR(36) NULL COMMENT '冲正单指向原发放单',
+ `reason` TEXT NULL COMMENT '拒绝、撤销或冲正原因',
+ `remark` TEXT NULL COMMENT '备注',
+ `created_at` BIGINT NOT NULL,
+ `updated_at` BIGINT NULL,
+ `deleted_at` BIGINT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_gao_package_delivery_request` (`store_id`, `request_no`),
+ UNIQUE KEY `uk_gao_package_delivery_revoke_source` (`source_delivery_id`),
+ KEY `idx_gao_package_delivery_package_time` (`customer_package_id`, `delivered_at`, `id`),
+ KEY `idx_gao_package_delivery_customer_time` (`store_id`, `customer_id`, `delivered_at`, `id`),
+ KEY `idx_gao_package_delivery_status` (`store_id`, `status`, `delivered_at`, `id`)
+) COMMENT='GAO 套餐发放操作';
+
+CREATE TABLE IF NOT EXISTS `gao_package_delivery_item` (
+ `id` VARCHAR(36) NOT NULL,
+ `delivery_id` VARCHAR(36) NOT NULL COMMENT '发放单 ID',
+ `customer_package_item_id` VARCHAR(36) NOT NULL COMMENT '客户套餐产品 ID',
+ `quantity` DECIMAL(18,3) NOT NULL COMMENT '本次数量',
+ `reserved_before` DECIMAL(18,3) NOT NULL COMMENT '操作前占用数量快照',
+ `reserved_after` DECIMAL(18,3) NOT NULL COMMENT '操作后占用数量快照',
+ `delivered_before` DECIMAL(18,3) NOT NULL COMMENT '操作前已发数量快照',
+ `delivered_after` DECIMAL(18,3) NOT NULL COMMENT '操作后已发数量快照',
+ `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照',
+ `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '产品单位快照',
+ `created_at` BIGINT NOT NULL,
+ `updated_at` BIGINT NULL,
+ `deleted_at` BIGINT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_gao_package_delivery_item` (`delivery_id`, `customer_package_item_id`),
+ KEY `idx_gao_package_delivery_item_package_item` (`customer_package_item_id`, `created_at`, `id`)
+) COMMENT='GAO 套餐发放产品明细';
+
+CREATE TABLE IF NOT EXISTS `gao_fund_record` (
+ `id` VARCHAR(36) NOT NULL,
+ `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
+ `customer_id` VARCHAR(36) NULL COMMENT '客户 ID',
+ `biz_type` VARCHAR(32) NOT NULL COMMENT '业务类型',
+ `biz_id` VARCHAR(36) NOT NULL COMMENT '业务对象 ID',
+ `operation_type` VARCHAR(32) NOT NULL COMMENT '资金操作类型',
+ `status` VARCHAR(32) NOT NULL COMMENT '资金操作状态',
+ `amount` BIGINT NOT NULL COMMENT '金额,单位分',
+ `source_record_id` VARCHAR(36) NULL COMMENT '冲正时关联原资金记录',
+ `request_no` VARCHAR(128) NOT NULL COMMENT '幂等请求号',
+ `operator_user_id` VARCHAR(36) NOT NULL COMMENT '提交操作用户 ID',
+ `verifier_user_id` VARCHAR(36) NULL COMMENT '收款核销用户 ID',
+ `verified_at` BIGINT NULL COMMENT '收款核销时间',
+ `occurred_at` BIGINT NOT NULL COMMENT '线下操作发生时间',
+ `reason` TEXT NULL COMMENT '原因',
+ `remark` TEXT NULL COMMENT '备注',
+ `created_at` BIGINT NOT NULL,
+ `updated_at` BIGINT NULL,
+ `deleted_at` BIGINT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_gao_fund_record_request` (`store_id`, `request_no`),
+ UNIQUE KEY `uk_gao_fund_record_revoke_source` (`source_record_id`),
+ KEY `idx_gao_fund_record_biz_time` (`store_id`, `biz_type`, `biz_id`, `occurred_at`, `id`),
+ KEY `idx_gao_fund_record_customer_time` (`store_id`, `customer_id`, `occurred_at`, `id`),
+ KEY `idx_gao_fund_record_status` (`store_id`, `status`, `operation_type`, `occurred_at`, `id`)
+) COMMENT='GAO 通用资金操作流水';
diff --git a/src/main/resources/db/migration/timiserver/V49__add_gao_event_record_updated_by.sql b/src/main/resources/db/migration/timiserver/V49__add_gao_event_record_updated_by.sql
new file mode 100644
index 0000000..5706015
--- /dev/null
+++ b/src/main/resources/db/migration/timiserver/V49__add_gao_event_record_updated_by.sql
@@ -0,0 +1,9 @@
+ALTER TABLE `gao_event_record`
+ ADD COLUMN `updated_by` VARCHAR(36) NULL COMMENT '最后修改用户 ID' AFTER `updated_at`;
+
+UPDATE `gao_event_record`
+SET `updated_by` = `operator_user_id`
+WHERE `updated_by` IS NULL;
+
+ALTER TABLE `gao_event_record`
+ MODIFY COLUMN `updated_by` VARCHAR(36) NOT NULL COMMENT '最后修改用户 ID';
diff --git a/src/main/resources/db/migration/timiserver/V51__bind_gao_package_purchase_event.sql b/src/main/resources/db/migration/timiserver/V51__bind_gao_package_purchase_event.sql
new file mode 100644
index 0000000..0fd52d5
--- /dev/null
+++ b/src/main/resources/db/migration/timiserver/V51__bind_gao_package_purchase_event.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `gao_event`
+ ADD COLUMN `package_config_id` VARCHAR(36) NULL COMMENT '购买套餐事件绑定的套餐配置 ID' AFTER `biz_type`,
+ ADD KEY `idx_gao_event_package_config` (`package_config_id`, `deleted_at`);
diff --git a/src/main/resources/db/migration/timiserver/V52__remove_gao_store_manager_inheritance.sql b/src/main/resources/db/migration/timiserver/V52__remove_gao_store_manager_inheritance.sql
new file mode 100644
index 0000000..f9abc4e
--- /dev/null
+++ b/src/main/resources/db/migration/timiserver/V52__remove_gao_store_manager_inheritance.sql
@@ -0,0 +1,10 @@
+UPDATE `role_permission_inherit` inherit
+JOIN `role` parent_role ON parent_role.id = inherit.parent_role_id
+JOIN `role` child_role ON child_role.id = inherit.child_role_id
+SET inherit.deleted_at = UNIX_TIMESTAMP() * 1000
+WHERE
+ inherit.deleted_at IS NULL
+ AND parent_role.module_code = 'GAO'
+ AND parent_role.code = 'GLOBAL_MANAGER'
+ AND child_role.module_code = 'GAO'
+ AND child_role.code = 'STORE_MANAGER';
diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml
new file mode 100644
index 0000000..fbb8168
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+ UPDATE `gao_customer_package_item`
+ SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
+ WHERE `id` = #{id} AND `deleted_at` IS NULL
+ AND `reserved_quantity` + #{delta} >= 0
+ AND `reserved_quantity` + `delivered_quantity` + #{delta} <= `total_quantity`
+
+
+ UPDATE `gao_customer_package_item`
+ SET `delivered_quantity` = `delivered_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
+ WHERE `id` = #{id} AND `deleted_at` IS NULL
+ AND `delivered_quantity` + #{delta} >= 0
+ AND `reserved_quantity` + `delivered_quantity` + #{delta} <= `total_quantity`
+
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml
new file mode 100644
index 0000000..d6696fc
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+ p.`deleted_at` IS NULL
+ AND p.`store_id` = #{storeId}
+ AND p.`customer_id` = #{customerId}
+ AND p.`status` = #{status}
+ AND p.`payment_status` = #{paymentStatus}
+ AND p.`refund_status` = #{refundStatus}
+ AND p.`purchased_at` >= #{beginAt}
+ AND p.`purchased_at` <= #{endAt}
+ AND (p.`package_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR c.`name` LIKE CONCAT('%', #{keyword}, '%') OR c.`code` LIKE CONCAT('%', #{keyword}, '%'))
+
+
+
+
+ UPDATE `gao_customer_package`
+ SET `pending_paid_amount` = `pending_paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
+ WHERE `id` = #{id} AND `deleted_at` IS NULL AND `pending_paid_amount` + #{delta} >= 0
+
+
+ UPDATE `gao_customer_package`
+ SET `paid_amount` = `paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
+ WHERE `id` = #{id} AND `deleted_at` IS NULL AND `paid_amount` + #{delta} >= 0
+
+
+ UPDATE `gao_customer_package`
+ SET `status` = #{toStatus}, `cancelled_at` = #{cancelledAt}, `cancel_operator_user_id` = #{cancelOperatorUserId}, `cancel_reason` = #{cancelReason}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
+ WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL
+
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml
index f8b7fd9..869d9c6 100644
--- a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml
+++ b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml
@@ -65,6 +65,7 @@
`verification_status` = 'VERIFIED',
`verifier_user_id` = #{verifierUserId},
`verified_at` = #{verifiedAt},
+ `updated_by` = #{updatedBy},
`updated_at` = ROUND(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000)
WHERE
`id` = #{id}
@@ -136,6 +137,9 @@
AND (
c.`code` LIKE CONCAT('%', #{keyword}, '%')
OR c.`name` LIKE CONCAT('%', #{keyword}, '%')
+ OR c.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%')
+ OR c.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%')
+ OR c.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%')
OR r.`remark` LIKE CONCAT('%', #{keyword}, '%')
OR r.`operator_user_id` LIKE CONCAT('%', #{keyword}, '%')
)
diff --git a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml
new file mode 100644
index 0000000..9b445d6
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+ f.`deleted_at` IS NULL
+ AND f.`store_id` = #{storeId}
+ AND f.`biz_id` = #{customerPackageId}
+ AND f.`customer_id` = #{customerId}
+ AND f.`operation_type` = #{operationType}
+ AND f.`status` = #{status}
+ AND f.`occurred_at` >= #{beginAt}
+ AND f.`occurred_at` <= #{endAt}
+
+
+
+
+ UPDATE `gao_fund_record`
+ SET `status` = #{toStatus}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
+ WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL
+
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageConfigItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigItemMapper.xml
new file mode 100644
index 0000000..1bb3f37
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigItemMapper.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageConfigMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigMapper.xml
new file mode 100644
index 0000000..3df1444
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoPackageConfigMapper.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml
new file mode 100644
index 0000000..3c723c7
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml
new file mode 100644
index 0000000..3f2c6c4
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+ d.`deleted_at` IS NULL
+ AND d.`store_id` = #{storeId}
+ AND d.`customer_package_id` = #{customerPackageId}
+ AND d.`customer_id` = #{customerId}
+ AND d.`status` = #{status}
+ AND d.`operation_type` = #{operationType}
+ AND d.`delivered_at` >= #{beginAt}
+ AND d.`delivered_at` <= #{endAt}
+
+
+
+
+ UPDATE `gao_package_delivery`
+ SET `status` = #{toStatus}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
+ WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL
+
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoStoreDiaryMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoStoreDiaryMapper.xml
new file mode 100644
index 0000000..29c3f32
--- /dev/null
+++ b/src/main/resources/mapper/timi-server/gao/GaoStoreDiaryMapper.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ `biz_type` = 'GAO_STORE_DIARY'
+ AND `biz_id` = #{storeId}
+ AND `created_at` >= #{beginCreatedAt}
+ AND `created_at` < #{endCreatedAt}
+ AND (`deleted_at` IS NULL OR FLOOR(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000) < `deleted_at`)
+
+
+
+
+
+
diff --git a/src/main/resources/mapper/timi-server/user/RolePermissionMapper.xml b/src/main/resources/mapper/timi-server/user/RolePermissionMapper.xml
index 3c223c7..48ce336 100644
--- a/src/main/resources/mapper/timi-server/user/RolePermissionMapper.xml
+++ b/src/main/resources/mapper/timi-server/user/RolePermissionMapper.xml
@@ -16,7 +16,7 @@
SELECT DISTINCT
permission.*
FROM `role_permission`
- LEFT JOIN `permission` ON permission.id = role_permission.permission_id
+ INNER JOIN `permission` ON permission.id = role_permission.permission_id
WHERE
`role_permission`.`role_id` IN