add GaoPackage

This commit is contained in:
Timi
2026-09-01 01:16:52 +08:00
parent b4b6a13f8d
commit eeb7b5b32e
24 changed files with 272 additions and 49 deletions
@@ -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,
@@ -73,7 +73,7 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
CUSTOMER_PACKAGE_CANCEL("CUSTOMER_PACKAGE:CANCEL", "客户套餐取消", true, true),
CUSTOMER_PACKAGE_PRICE_OVERRIDE("CUSTOMER_PACKAGE:PRICE_OVERRIDE", "客户套餐成交价和产品定制", true, true),
CUSTOMER_PACKAGE_PRICE_OVERRIDE("CUSTOMER_PACKAGE:PRICE_OVERRIDE", "客户套餐成交价和权益配额调整", true, true),
PACKAGE_DELIVERY_CREATE("PACKAGE_DELIVERY:CREATE", "套餐发放创建", true, true),
@@ -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;
@@ -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
@@ -69,15 +73,38 @@ public class GaoEventController {
event.setRoleCodeList(roleMap.get(event.getId()));
}
service.attachOptionGroupList(result.getList());
attachPackageConfig(result.getList());
return result;
}
private void attachPackageConfig(List<GaoEvent> eventList) {
List<String> packageConfigIdList = eventList.stream()
.map(GaoEvent::getPackageConfigId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
if (packageConfigIdList.isEmpty()) {
return;
}
Map<String, GaoPackageConfig> packageConfigMap = packageConfigService.mapByIdList(packageConfigIdList);
Map<String, List<GaoPackageConfigItem>> 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<GaoEvent> listValid(@RequestParam String storeId) {
return service.listValid(storeScopeService.resolveStoreId(storeId));
List<GaoEvent> result = service.listValid(storeScopeService.resolveStoreId(storeId));
attachPackageConfig(result);
return result;
}
@AOPLog
@@ -90,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;
}
@@ -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()));
}
@@ -106,6 +106,13 @@ public class GaoEvent extends UUIDEntity {
/// 业务类型
private BizType bizType;
/// 购买套餐事件绑定的套餐配置 ID
private String packageConfigId;
/// 购买套餐事件绑定的套餐配置
@Transient
private GaoPackageConfig packageConfig;
/// 登记事件名称
private String name;
@@ -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<Attachment> attachmentList;
@@ -4,6 +4,7 @@ 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;
@@ -25,4 +26,7 @@ public interface GaoEventMapper extends BaseMapper<GaoEvent, String> {
List<GaoEvent> selectByIdList(Collection<String> idList);
List<GaoEvent> 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);
}
@@ -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<GaoEventRecord, String> {
/// 查询事件登记记录数量
///
/// @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<GaoEventRecord, String>
/// @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);
/// 按门店和登记日期统计登记记录数
///
@@ -22,6 +22,12 @@ public interface GaoPackageConfigService extends BaseService<GaoPackageConfig, S
/// 查询有效可购买套餐
List<GaoPackageConfig> listValid(String storeId);
/// 批量查询套餐配置
///
/// @param idList 套餐配置 ID 列表
/// @return 按套餐配置 ID 映射的配置
Map<String, GaoPackageConfig> mapByIdList(Collection<String> idList);
/// 查询套餐配置产品明细
///
/// @param packageConfigId 套餐配置 ID
@@ -171,23 +171,25 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
TimiException.required(request, "not found purchase request");
TimiException.required(request.getRequestNo(), "not found requestNo");
request.setRequestNo(request.getRequestNo().trim());
TimiException.required(request.getPackageConfigId(), "not found packageConfigId");
TimiException.required(request.getEventId(), "not found eventId");
TimiException.required(request.getCustomerId(), "not found customerId");
TimiException.required(request.getSaleAmount(), "not found saleAmount");
TimiException.requiredTrue(0 <= request.getSaleAmount(), "成交价不能为负数");
GaoPackageConfig config = packageConfigService.get(request.getPackageConfigId());
if (config == null) {
GaoCustomerPackage existing = mapper.selectByRequestNoAnyStore(request.getRequestNo());
if (existing != null) {
storeScopeService.checkStoreId(existing.getStoreId());
verifyPurchaseIdempotency(existing, request);
return existing;
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<GaoPackageConfigItem> 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));
}
}
TimiException.required(config, "not found package config");
storeScopeService.checkStoreId(config.getStoreId());
List<GaoCustomerPackagePurchaseItem> 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);
@@ -199,17 +201,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买");
TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效");
TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期");
List<GaoPackageConfigItem> configItemList = packageConfigService.listItemByPackageConfigId(config.getId());
GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId());
GaoEvent event = validatePurchaseEvent(request.getEventId(), config.getStoreId());
List<GaoCustomerPackagePurchaseItem> itemList = new java.util.ArrayList<>(TimiJava.defaultIfNull(request.getItemList(), List.of()));
verifyPurchaseItems(itemList);
itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort));
boolean canOverride = permissionChecker.hasAny(ModuleCode.GAO, com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
if (!canOverride) {
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()), "普通员工不能修改套餐成交价");
verifySameAsConfig(itemList, configItemList);
verifySameQuantityAsConfig(itemList, configItemList);
}
GaoEventRecord eventRecord = new GaoEventRecord();
@@ -299,14 +296,26 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
return customer;
}
private GaoEvent validatePurchaseEvent(String eventId, String storeId) {
private GaoEvent validatePurchaseEvent(String eventId) {
GaoEvent event = eventService.get(eventId);
TimiException.required(event, "not found purchase event");
TimiException.requiredTrue(storeId.equals(event.getStoreId()), "购买事件和套餐不属于同一门店");
TimiException.requiredTrue(event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE, "事件不是套餐购买事件");
TimiException.required(event.getPackageConfigId(), "购买事件未绑定套餐配置");
return event;
}
private List<GaoCustomerPackagePurchaseItem> defaultPurchaseItems(List<GaoPackageConfigItem> 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<GaoCustomerPackagePurchaseItem> itemList) {
TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品");
Set<String> keySet = new HashSet<>();
@@ -326,20 +335,27 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
}
}
private void verifySameAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
TimiException.requiredTrue(itemList.size() == configItemList.size(), "普通员工不能修改套餐产品明细");
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致");
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit());
TimiException.required(item, "普通员工不能修改套餐产品明细");
TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0
&& item.getSort().equals(configItem.getSort()), "普通员工不能修改套餐产品明细");
TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致");
TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
&& sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改");
}
}
private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity()));
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.getPackageConfigId().equals(request.getPackageConfigId())
&& existing.getCustomerId().equals(request.getCustomerId())
TimiException.requiredTrue(existing.getCustomerId().equals(request.getCustomerId())
&& existing.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用");
GaoEventRecord eventRecord = eventRecordService.get(existing.getPurchaseEventRecordId());
TimiException.required(eventRecord, "购买事件登记不存在");
@@ -354,7 +370,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
for (GaoCustomerPackagePurchaseItem item : requestItemList) {
GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductName() + "|" + item.getUnit());
TimiException.required(existingItem, "requestNo 对应购买请求内容不一致");
TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0 && existingItem.getSort().equals(item.getSort()), "requestNo 对应购买请求内容不一致");
TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0
&& existingItem.getSort().equals(item.getSort())
&& sameText(existingItem.getRemark(), item.getRemark()), "requestNo 对应购买请求内容不一致");
}
}
@@ -139,7 +139,8 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
GaoEventRecord record = get(id);
TimiException.required(record, "not found event record");
TimiException.requiredTrue(record.getVerificationStatus() == GaoEventRecord.VerificationStatus.PENDING, "登记记录无需核销或已核销");
int updated = mapper.verify(id, userLoginService.getRequireLoginUserId(), Time.now());
String loginUserId = userLoginService.getRequireLoginUserId();
int updated = mapper.verify(id, loginUserId, Time.now(), loginUserId);
TimiException.requiredTrue(0 < updated, "登记记录无需核销或已核销");
logger.setLevel(Logger.Level.INFO);
logger.setResult(id);
@@ -175,7 +176,6 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
return createInternal(record, true);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
private GaoEventRecord createInternal(GaoEventRecord record, boolean packagePurchase) {
TimiException.required(record, "not found record");
@@ -224,6 +224,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
dbRecord.setStoreId(event.getStoreId());
dbRecord.setEventId(record.getEventId());
dbRecord.setOperatorUserId(loginUserId);
dbRecord.setUpdatedBy(loginUserId);
dbRecord.setRemark(record.getRemark());
dbRecord.setRegisteredAt(registeredAt);
dbRecord.setRegisteredDateValue(registeredDateValue);
@@ -318,6 +319,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
verifyRecordAttachment(event, record.getAttachmentList());
checkEventPermission(event);
dbRecord.setRemark(record.getRemark());
dbRecord.setUpdatedBy(userLoginService.getRequireLoginUserId());
super.update(dbRecord);
if (canonicalOptionList != null) {
GaoEventRecordOption optionExample = new GaoEventRecordOption();
@@ -6,12 +6,14 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventOption;
import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup;
import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.mapper.GaoEventMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventOptionGroupMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventOptionMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventRoleRelationMapper;
import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.java.TimiJava;
@@ -57,6 +59,7 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
private final GaoEventOptionMapper optionMapper;
private final GaoEventRecordMapper eventRecordMapper;
private final GaoEventRoleRelationMapper roleRelationMapper;
private final GaoPackageConfigService packageConfigService;
@Override
protected BaseMapper<GaoEvent, String> mapper() {
@@ -130,6 +133,10 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
GaoEvent.BizType bizType = TimiJava.defaultIfNull(event.getBizType(), dbEvent.getBizType());
TimiException.requiredTrue(bizType == TimiJava.defaultIfNull(dbEvent.getBizType(), GaoEvent.BizType.GENERAL), "事件业务类型创建后不可修改");
dbEvent.setBizType(bizType);
if (event.getPackageConfigId() != null && !event.getPackageConfigId().equals(dbEvent.getPackageConfigId())) {
TimiException.requiredTrue(0 == eventRecordMapper.countByEventId(dbEvent.getId()), "已有登记记录的事件不能更换绑定套餐");
dbEvent.setPackageConfigId(event.getPackageConfigId());
}
dbEvent.setValueType(TimiJava.defaultIfNull(event.getValueType(), oldValueType));
if (event.getValueUnit() != null) {
dbEvent.setValueUnit(event.getValueUnit());
@@ -317,6 +324,12 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
event.setBizType(TimiJava.defaultIfNull(event.getBizType(), GaoEvent.BizType.GENERAL));
if (event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE) {
TimiException.requiredTrue(!Boolean.TRUE.equals(event.getRequireVerification()), "购买套餐事件不能要求核销");
TimiException.required(event.getPackageConfigId(), "购买套餐事件必须绑定套餐配置");
GaoPackageConfig packageConfig = packageConfigService.get(event.getPackageConfigId());
TimiException.required(packageConfig, "购买套餐事件绑定的套餐配置不存在");
TimiException.requiredTrue(event.getStoreId().equals(packageConfig.getStoreId()), "购买事件和套餐配置不属于同一门店");
} else {
TimiException.requiredNull(event.getPackageConfigId(), "普通事件不能绑定套餐配置");
}
GaoEvent.ValueType valueType = TimiJava.defaultIfNull(event.getValueType(), GaoEvent.ValueType.NONE);
if (valueType == GaoEvent.ValueType.NONE) {
@@ -7,6 +7,7 @@ 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;
@@ -37,6 +38,7 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
private final GaoPackageConfigMapper mapper;
private final GaoPackageConfigItemMapper itemMapper;
private final GaoEventMapper eventMapper;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@@ -60,6 +62,15 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
return mapper.selectValidList(storeId, Time.now());
}
@Override
public Map<String, GaoPackageConfig> mapByIdList(Collection<String> 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<GaoPackageConfigItem> listItemByPackageConfigId(String packageConfigId) {
TimiException.required(packageConfigId, "not found packageConfigId");
@@ -134,6 +145,7 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
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;
});
@@ -182,7 +182,7 @@ public class GaoPackageDeliveryServiceImplement extends AbstractEntityService<Ga
BigDecimal reservedBefore = decimal(packageItem.getReservedQuantity());
BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity());
BigDecimal available = decimal(packageItem.getTotalQuantity()).subtract(reservedBefore).subtract(deliveredBefore);
TimiException.requiredTrue(0 < requestItem.getQuantity().compareTo(available), "产品可发数量不足");
TimiException.requiredTrue(0 <= available.compareTo(requestItem.getQuantity()), "产品可发数量不足");
int updated = customerPackageService.updateItemReservedQuantity(packageItem.getId(), requestItem.getQuantity());
TimiException.requiredTrue(0 < updated, "产品可发数量不足");
GaoPackageDeliveryItem savedItem = copyDeliveryItem(requestItem);
@@ -21,16 +21,13 @@ public class GaoCustomerPackagePurchaseRequest {
/// 客户 ID
private String customerId;
/// 套餐配置 ID
private String packageConfigId;
/// 实际成交价,单位分
/// 实际成交价,单位分,空为事件绑定套餐的标准价
private Long saleAmount;
/// 购买时间
private Long purchasedAt;
/// 最终产品明细
/// 最终产品明细,空为事件绑定套餐的默认产品明细
private List<GaoCustomerPackagePurchaseItem> itemList;
/// 购买照片
@@ -206,6 +206,7 @@ public class InitBuiltinUser implements ApplicationRunner {
private void bindBuiltinRoleParent(Map<String, Role> roleMap) {
setRoleParent(roleMap, ModuleCode.CORE, CoreRoleCode.ADMIN.name(), ModuleCode.CORE, CoreRoleCode.SYSTEM.name());
setRoleParent(roleMap, ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name(), ModuleCode.CORE, CoreRoleCode.ADMIN.name());
setRoleParent(roleMap, ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name(), ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name());
}
/// 按权限定义补齐 SYSTEM 和 ADMIN 的默认权限
@@ -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");
}
}
}
}
@@ -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';
@@ -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`);
@@ -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';
@@ -25,11 +25,15 @@
<update id="updateReservedQuantity">
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} &gt;= 0
WHERE `id` = #{id} AND `deleted_at` IS NULL
AND `reserved_quantity` + #{delta} &gt;= 0
AND `reserved_quantity` + `delivered_quantity` + #{delta} &lt;= `total_quantity`
</update>
<update id="updateDeliveredQuantity">
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} &gt;= 0
WHERE `id` = #{id} AND `deleted_at` IS NULL
AND `delivered_quantity` + #{delta} &gt;= 0
AND `reserved_quantity` + `delivered_quantity` + #{delta} &lt;= `total_quantity`
</update>
</mapper>
@@ -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}
@@ -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
<foreach collection="roleIdList" item="item" separator="," open="(" close=")">