From af76eb43b83c06983249fbdd3250db7bd49e5a32 Mon Sep 17 00:00:00 2001 From: Timi Date: Tue, 8 Sep 2026 16:52:32 +0800 Subject: [PATCH 1/5] update SystemSetting --- .idea/data_source_mapping.xml | 6 + .../imyeyu/api/annotation/EnableSetting.java | 35 -- .../annotation/EnableSettingInterceptor.java | 55 ---- .../imyeyu/api/annotation/SettingBizType.java | 17 + .../java/com/imyeyu/api/bean/SettingEnum.java | 27 ++ .../java/com/imyeyu/api/config/WebConfig.java | 3 - .../modules/common/bean/SettingModule.java | 52 --- .../modules/common/bean/TempFileSetting.java | 41 +++ .../common/controller/SettingController.java | 3 + .../api/modules/common/entity/Setting.java | 308 +----------------- .../common/service/SettingService.java | 6 +- .../implement/TempFileServiceImplement.java | 11 +- .../journal/controller/ToolController.java | 9 +- .../api/modules/music/bean/MusicSetting.java | 44 +++ .../runner/ControllerBootstrapRunner.java | 14 +- .../music/runner/PlayerBootstrapRunner.java | 10 +- .../system/bean/SystemStatusSetting.java | 32 ++ .../implement/StatusServiceImplement.java | 10 +- .../status/AbstractDequeStatusCollector.java | 4 +- .../collector/NetworkStatusCollector.java | 8 +- .../api/modules/user/bean/UserSetting.java | 35 ++ .../implement/UserLoginServiceImplement.java | 4 +- .../imyeyu/api/util/InitBuiltinSetting.java | 82 +++++ 23 files changed, 329 insertions(+), 487 deletions(-) create mode 100644 .idea/data_source_mapping.xml delete mode 100644 src/main/java/com/imyeyu/api/annotation/EnableSetting.java delete mode 100644 src/main/java/com/imyeyu/api/annotation/EnableSettingInterceptor.java create mode 100644 src/main/java/com/imyeyu/api/annotation/SettingBizType.java create mode 100644 src/main/java/com/imyeyu/api/bean/SettingEnum.java delete mode 100644 src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java create mode 100644 src/main/java/com/imyeyu/api/modules/common/bean/TempFileSetting.java create mode 100644 src/main/java/com/imyeyu/api/modules/music/bean/MusicSetting.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/bean/SystemStatusSetting.java create mode 100644 src/main/java/com/imyeyu/api/modules/user/bean/UserSetting.java create mode 100644 src/main/java/com/imyeyu/api/util/InitBuiltinSetting.java diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml new file mode 100644 index 0000000..a381639 --- /dev/null +++ b/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/com/imyeyu/api/annotation/EnableSetting.java b/src/main/java/com/imyeyu/api/annotation/EnableSetting.java deleted file mode 100644 index f1d9da3..0000000 --- a/src/main/java/com/imyeyu/api/annotation/EnableSetting.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.imyeyu.api.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * 启用配置注解 - * - * @author 夜雨 - * @since 2023-07-15 10:00 - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface EnableSetting { - - /** - * - * - * @author 夜雨 - * @since 2026-04-27 14:45 - */ - enum Logic { - - AND, - - OR - } - - Logic logic() default Logic.AND; - - /** @return 未启用配置时响应消息语言映射键 */ - String message() default "service.offline"; -} diff --git a/src/main/java/com/imyeyu/api/annotation/EnableSettingInterceptor.java b/src/main/java/com/imyeyu/api/annotation/EnableSettingInterceptor.java deleted file mode 100644 index f2f2987..0000000 --- a/src/main/java/com/imyeyu/api/annotation/EnableSettingInterceptor.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.imyeyu.api.annotation; - -import com.imyeyu.api.modules.common.service.SettingService; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Lazy; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; - -/** - * 启用配置注解处理器 - * - * @author 夜雨 - * @since 2023-07-15 10:01 - */ -@Component -@RequiredArgsConstructor(onConstructor_ = {@Lazy}) -public class EnableSettingInterceptor implements HandlerInterceptor { - - private final SettingService service; - - public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) throws InvocationTargetException, IllegalAccessException { - if (handler instanceof HandlerMethod handlerMethod) { - EnableSetting annotation = handlerMethod.getMethodAnnotation(EnableSetting.class); - if (annotation == null) { - return true; - } - EnableSetting.Logic logic = annotation.logic(); - List valueList = new ArrayList<>(); - for (Method method : annotation.getClass().getMethods()) { - if (method.getName().equals("message")) { - continue; - } - Enum[] enums = (Enum[]) method.invoke(annotation); - for (Enum anEnum : enums) { - valueList.add(service.getSystem(anEnum).isTrue()); - } - } - if (logic == EnableSetting.Logic.AND) { - return valueList.stream().allMatch(Boolean::booleanValue); - } else { - return valueList.stream().anyMatch(Boolean::booleanValue); - } - } - return true; - } -} diff --git a/src/main/java/com/imyeyu/api/annotation/SettingBizType.java b/src/main/java/com/imyeyu/api/annotation/SettingBizType.java new file mode 100644 index 0000000..323505a --- /dev/null +++ b/src/main/java/com/imyeyu/api/annotation/SettingBizType.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// +/// +/// @author 夜雨 +/// @since 2026-09-08 15:22 +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface SettingBizType { + + String value(); +} diff --git a/src/main/java/com/imyeyu/api/bean/SettingEnum.java b/src/main/java/com/imyeyu/api/bean/SettingEnum.java new file mode 100644 index 0000000..8cad560 --- /dev/null +++ b/src/main/java/com/imyeyu/api/bean/SettingEnum.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.bean; + +import com.imyeyu.api.annotation.SettingBizType; +import com.imyeyu.api.modules.common.entity.Setting; +import com.imyeyu.utils.Text; + +/// +/// +/// @author 夜雨 +/// @since 2026-09-08 15:18 +public interface SettingEnum { + + default String getBizType() { + Class implEnum = getClass(); + SettingBizType bizType = implEnum.getAnnotation(SettingBizType.class); + if (bizType == null) { + return Text.bizClassName(implEnum); + } + return bizType.value(); + } + + String name(); + + String getDefaultValue(); + + Setting.ValueType getValueType(); +} diff --git a/src/main/java/com/imyeyu/api/config/WebConfig.java b/src/main/java/com/imyeyu/api/config/WebConfig.java index dbf545c..89441bb 100644 --- a/src/main/java/com/imyeyu/api/config/WebConfig.java +++ b/src/main/java/com/imyeyu/api/config/WebConfig.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.imyeyu.api.annotation.EnableSettingInterceptor; import com.imyeyu.api.annotation.RequestRateLimitInterceptor; import com.imyeyu.api.annotation.RequireCorePermissionInterceptor; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor; @@ -40,7 +39,6 @@ public class WebConfig implements WebMvcConfigurer { private final ObjectMapper jackson; private final SystemAPIInterceptor systemAPIInterceptor; private final JournalAPIInterceptor journalAPIInterceptor; - private final EnableSettingInterceptor enableSettingInterceptor; private final RequestRateLimitInterceptor requestRateLimitInterceptor; private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor; private final RequireCorePermissionInterceptor requireCorePermissionInterceptor; @@ -54,7 +52,6 @@ public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH); registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH); - registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**"); registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**"); diff --git a/src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java b/src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java deleted file mode 100644 index 6af5d0d..0000000 --- a/src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.imyeyu.api.modules.common.bean; - -import com.imyeyu.api.modules.common.entity.Setting; -import com.imyeyu.java.bean.timi.TimiCode; -import com.imyeyu.java.bean.timi.TimiException; -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 设置模块 - * - * @author 夜雨 - * @since 2026-04-24 00:59 - */ -@Getter -@AllArgsConstructor -public enum SettingModule { - - COMMON(Setting.Module.Common.class), - - DOMAIN(Setting.Module.Domain.class), - - FOREVER_MC(Setting.Module.ForeverMC.class), - - MULTILINGUAL(Setting.Module.Multilingual.class), - - GITEA(Setting.Module.Gitea.class), - - MUSIC(Setting.Module.Music.class), - - JOURNAL(Setting.Module.Journal.class), - - TEMP_FILE(Setting.Module.TempFile.class), - - USER(Setting.Module.User.class), - - SYSTEM(Setting.Module.System.class), - - SYSTEM_STATUS(Setting.Module.SystemStatus.class); - - /** 模块枚举类 */ - final Class> clazz; - - public static > SettingModule fromClass(Class clazz) { - for (SettingModule value : values()) { - if (value.clazz.equals(clazz)) { - return value; - } - } - throw new TimiException(TimiCode.ERROR, "not found System Module: %s".formatted(clazz.getName())); - } -} diff --git a/src/main/java/com/imyeyu/api/modules/common/bean/TempFileSetting.java b/src/main/java/com/imyeyu/api/modules/common/bean/TempFileSetting.java new file mode 100644 index 0000000..93be145 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/bean/TempFileSetting.java @@ -0,0 +1,41 @@ +package com.imyeyu.api.modules.common.bean; + +import com.imyeyu.api.annotation.SettingBizType; +import com.imyeyu.api.bean.SettingEnum; +import com.imyeyu.api.modules.common.entity.Setting; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * + * + * @author 夜雨 + * @since 2026-04-24 14:47 + */ +@Getter +@SettingBizType("TEMP_FILE") +@AllArgsConstructor +public enum TempFileSetting implements SettingEnum { + + /** 启用服务 */ + ENABLE(Boolean.TRUE.toString(), Setting.ValueType.BOOLEAN), + + /** 最小缓存时长 */ + TTL_MIN("1h", Setting.ValueType.TIME), + + /** 默认缓存时长 */ + TTL_DEFAULT("3d", Setting.ValueType.TIME), + + /** 最大缓存时长 */ + TTL_MAX("7d", Setting.ValueType.TIME), + + /** 已过期文件残留时长 */ + RESIDUE_TTL("90d", Setting.ValueType.TIME), + + /** 每个 IP 限制有效临时文件容量 */ + LIMIT_SIZE_OF_IP("10 GB", Setting.ValueType.BYTE_SIZE); + + final String defaultValue; + + final Setting.ValueType valueType; +} \ No newline at end of file diff --git a/src/main/java/com/imyeyu/api/modules/common/controller/SettingController.java b/src/main/java/com/imyeyu/api/modules/common/controller/SettingController.java index eeb1b6c..27a9eec 100644 --- a/src/main/java/com/imyeyu/api/modules/common/controller/SettingController.java +++ b/src/main/java/com/imyeyu/api/modules/common/controller/SettingController.java @@ -53,6 +53,7 @@ public class SettingController { */ @JsonView(ResponseView.Admin.class) @RequestRateLimit + @RequireCorePermission(CorePermissionCode.SETTING_READ) @PostMapping("/detail") public Setting detail(@RequestBody String id) { return service.get(id); @@ -66,6 +67,7 @@ public class SettingController { */ @AOPLog @JsonView(ResponseView.Admin.class) + @RequireCorePermission(CorePermissionCode.SETTING_CREATE) @RequestRateLimit @PostMapping("/create") public Setting create(@RequestBody Setting setting) { @@ -80,6 +82,7 @@ public class SettingController { */ @AOPLog @RequestRateLimit + @RequireCorePermission(CorePermissionCode.SETTING_UPDATE) @PostMapping("/update") public void update(@RequestBody Setting setting) { service.update(setting); diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Setting.java b/src/main/java/com/imyeyu/api/modules/common/entity/Setting.java index 2800b0b..5442c0e 100644 --- a/src/main/java/com/imyeyu/api/modules/common/entity/Setting.java +++ b/src/main/java/com/imyeyu/api/modules/common/entity/Setting.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.imyeyu.api.TimiServerAPI; -import com.imyeyu.api.modules.common.bean.SettingModule; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.util.ResponseView; @@ -62,6 +61,9 @@ public class Setting extends UUIDEntity { /** 时间 */ TIME, + /// 字节大小 + BYTE_SIZE, + /** 时长 */ DURATION, @@ -102,16 +104,6 @@ public class Setting extends UUIDEntity { @Transient private Enum keyRaw; - public Setting(Enum key) { - setKeyRaw(key); - } - - public Setting(Enum key, Object value, ValueType valueType) { - setKeyRaw(key); - this.value = value.toString(); - this.valueType = valueType; - } - /** * 以字符串形式返回配置值 * @@ -221,298 +213,4 @@ public class Setting extends UUIDEntity { throw new IllegalArgumentException("setting value is not valid json: %s".formatted(value), e); } } - - public void setKeyRaw(Enum key) { - this.keyRaw = key; - this.bizType = SettingModule.fromClass(key.getClass()).toString(); - this.code = key.toString(); - } - - public boolean is(Enum key) { - return SettingModule.fromClass(key.getClass()).toString().equals(bizType) && key.toString().equals(code); - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:23 - */ - public static final class Module { - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 15:39 - */ - public enum Common { - - /** 微信收款码 */ - WECHAT_RECEIVE_QR_CODE - } - - /** - * 域名 - * - * @author 夜雨 - * @since 2026-04-26 17:18 - */ - public enum Domain { - - ROOT, - - API, - - GIT, - - BLOG, - - SPACE, - - FOREVER_MC - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 15:14 - */ - public enum Blog { - - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 15:03 - */ - public enum User { - - /** 启用登录 */ - ENABLE_LOGIN, - - /** 启用注册 */ - ENABLE_REGISTER, - - /** 允许数据更新 */ - ENABLE_PROFILE_UPDATE, - - /** 令牌有效期 */ - TOKEN_TTL - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 15:21 - */ - public enum Comment { - - /** 启用服务 */ - ENABLE - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 15:03 - */ - public enum ICP { - - IMYEYU_COM - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 15:01 - */ - public enum ForeverMC { - - PLAYER_BIND_MAX, - - LOGIN_TOKEN_TTL - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:57 - */ - public enum Multilingual { - - /** 翻译 API */ - TRANSLATE_API, - - /** 翻译 AppId */ - TRANSLATE_APP_ID, - - /** 翻译密钥 */ - TRANSLATE_KEY, - - /** 翻译计划任务表达式 */ - TRANSLATE_CORN, - - /** 缓存时长 */ - CACHE_TTL - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:55 - */ - public enum Gitea { - - /** 接口地址 */ - API, - - /** 关于页面所属文章 ID */ - ABOUT_ARTICLE_ID, - - /** 仓库文件地址 */ - REPOSITORY_PATH - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:52 - */ - public enum Music { - - /** 启用服务 */ - ENABLE, - - /** 通信消息最大帧长度 */ - MAX_FRAME_LENGTH, - - /** 受控端 IP */ - PLAYER_IP, - - /** 受控端端口 */ - PLAYER_PORT, - - /** 控制端 IP */ - CONTROLLER_IP, - - /** 控制端端口 */ - CONTROLLER_PORT, - - /** 控制端入口地址 */ - CONTROLLER_URI, - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:49 - */ - public enum Journal { - - /** 启用服务 */ - ENABLE, - - /** 审核中 */ - REVIEWING, - - /** 访问密钥 */ - KEY, - - /** 微信小程序 AppId */ - APP_ID, - - /** 微信小程序 AppSecret */ - APP_SECRET, - - /** 备忘录内容 */ - MEMO, - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:47 - */ - public enum TempFile { - - /** 启用服务 */ - ENABLE, - - /** 最小缓存时长 */ - TTL_MIN, - - /** 默认缓存时长 */ - TTL_DEFAULT, - - /** 最大缓存时长 */ - TTL_MAX, - - /** 已过期文件残留时长 */ - RESIDUE_TTL, - - /** 每个 IP 限制有效临时文件容量 */ - LIMIT_SIZE_OF_IP - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-27 14:35 - */ - public enum FileSync { - - ENABLE, - - CRON, - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:23 - */ - public enum System { - - /** 文件系统基准路径 */ - FILE_BASE_PATH, - - /** 设置缓存时长 */ - SETTING_TTL, - - /** 哀悼滤镜 */ - ENABLE_MOURN_FILTER, - } - - /** - * - * - * @author 夜雨 - * @since 2026-04-24 14:23 - */ - public enum SystemStatus { - - /** 状态采集数据量 */ - LIMIT, - - /** 状态采集网卡 */ - NETWORK_MAC, - - /** 状态采集频率 */ - RATE, - } - } } diff --git a/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java b/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java index 6333371..3032d99 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java @@ -1,6 +1,6 @@ package com.imyeyu.api.modules.common.service; -import com.imyeyu.api.modules.common.bean.SettingModule; +import com.imyeyu.api.bean.SettingEnum; import com.imyeyu.api.modules.common.bean.attachment.SettingSupport; import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.java.TimiJava; @@ -18,8 +18,8 @@ public interface SettingService extends BaseService { Setting getByCode(String bizType, String bizId, String code); - default Setting getSystem(Enum bizType) { - return getByCode(SettingModule.fromClass(bizType.getClass()).name(), TimiJava.ZERO_UUID, bizType.toString()); + default Setting getSystem(SettingEnum setting) { + return getByCode(setting.getBizType(), TimiJava.ZERO_UUID, setting.name()); } List listByBizId(String bizType, String bizId); diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java index bc9f403..24c8aa8 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java @@ -3,6 +3,7 @@ package com.imyeyu.api.modules.common.service.implement; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.imyeyu.api.modules.common.bean.TempFileSetting; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.service.AttachmentService; @@ -52,11 +53,11 @@ public class TempFileServiceImplement implements TempFileService { try { logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl))); - Setting ttlMin = settingService.getSystem(Setting.Module.TempFile.TTL_MIN); - Setting ttlMax = settingService.getSystem(Setting.Module.TempFile.TTL_MAX); - Setting ttlDefault = settingService.getSystem(Setting.Module.TempFile.TTL_DEFAULT); - Setting residueTime = settingService.getSystem(Setting.Module.TempFile.RESIDUE_TTL); - String limitStr = settingService.getSystem(Setting.Module.TempFile.LIMIT_SIZE_OF_IP).asString(); + Setting ttlMin = settingService.getSystem(TempFileSetting.TTL_MIN); + Setting ttlMax = settingService.getSystem(TempFileSetting.TTL_MAX); + Setting ttlDefault = settingService.getSystem(TempFileSetting.TTL_DEFAULT); + Setting residueTime = settingService.getSystem(TempFileSetting.RESIDUE_TTL); + String limitStr = settingService.getSystem(TempFileSetting.LIMIT_SIZE_OF_IP).asString(); long limit = IOSize.parse(limitStr); diff --git a/src/main/java/com/imyeyu/api/modules/journal/controller/ToolController.java b/src/main/java/com/imyeyu/api/modules/journal/controller/ToolController.java index e5ef910..42755ae 100644 --- a/src/main/java/com/imyeyu/api/modules/journal/controller/ToolController.java +++ b/src/main/java/com/imyeyu/api/modules/journal/controller/ToolController.java @@ -25,14 +25,15 @@ public class ToolController { @GetMapping("/memo") public String getMemo() { - return settingService.getSystem(Setting.Module.Journal.MEMO).asString(); +// return settingService.getSystem(Setting.Module.Journal.MEMO).asString(); + return null; } @RequiredUploadPermission @PostMapping("/memo/update") public void updateMemo(@RequestBodyValue String data) { - Setting setting = settingService.getSystem(Setting.Module.Journal.MEMO); - setting.setValue(data); - settingService.update(setting); +// Setting setting = settingService.getSystem(Setting.Module.Journal.MEMO); +// setting.setValue(data); +// settingService.update(setting); } } diff --git a/src/main/java/com/imyeyu/api/modules/music/bean/MusicSetting.java b/src/main/java/com/imyeyu/api/modules/music/bean/MusicSetting.java new file mode 100644 index 0000000..d38e1eb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/music/bean/MusicSetting.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.music.bean; + +import com.imyeyu.api.annotation.SettingBizType; +import com.imyeyu.api.bean.SettingEnum; +import com.imyeyu.api.modules.common.entity.Setting; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * + * + * @author 夜雨 + * @since 2026-04-24 14:52 + */ +@Getter +@SettingBizType("MUSIC") +@AllArgsConstructor +public enum MusicSetting implements SettingEnum { + + /** 启用服务 */ + ENABLE(Boolean.FALSE.toString(), Setting.ValueType.BOOLEAN), + + /** 通信消息最大帧长度 */ + MAX_FRAME_LENGTH("2048", Setting.ValueType.INTEGER), + + /** 受控端 IP */ + PLAYER_IP("0.0.0.0", Setting.ValueType.STRING), + + /** 受控端端口 */ + PLAYER_PORT("18392", Setting.ValueType.INTEGER), + + /** 控制端 IP */ + CONTROLLER_IP("0.0.0.0", Setting.ValueType.STRING), + + /** 控制端端口 */ + CONTROLLER_PORT("18391", Setting.ValueType.INTEGER), + + /** 控制端入口地址 */ + CONTROLLER_URI("/music/control", Setting.ValueType.STRING); + + final String defaultValue; + + final Setting.ValueType valueType; +} diff --git a/src/main/java/com/imyeyu/api/modules/music/runner/ControllerBootstrapRunner.java b/src/main/java/com/imyeyu/api/modules/music/runner/ControllerBootstrapRunner.java index 9534539..20d57f7 100644 --- a/src/main/java/com/imyeyu/api/modules/music/runner/ControllerBootstrapRunner.java +++ b/src/main/java/com/imyeyu/api/modules/music/runner/ControllerBootstrapRunner.java @@ -1,8 +1,8 @@ package com.imyeyu.api.modules.music.runner; import com.imyeyu.api.config.CORSConfig; -import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.service.SettingService; +import com.imyeyu.api.modules.music.bean.MusicSetting; import com.imyeyu.api.modules.music.handler.ControllerMessageHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; @@ -62,7 +62,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application } public void run(ApplicationArguments args) { - if (settingService.getSystem(Setting.Module.Music.ENABLE).isTrue()) { + if (settingService.getSystem(MusicSetting.ENABLE).isTrue()) { return; } @@ -75,7 +75,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(boss, worker); bootstrap.channel(NioServerSocketChannel.class); - bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(Setting.Module.Music.CONTROLLER_PORT).asInteger())); + bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(MusicSetting.CONTROLLER_PORT).asInteger())); bootstrap.childHandler(new ChannelInitializer() { @Override @@ -89,7 +89,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application @Override public void channelRead(ChannelHandlerContext ctx, Object obj) throws Exception { if (obj instanceof FullHttpRequest req) { - if (!req.uri().equals(settingService.getSystem(Setting.Module.Music.CONTROLLER_URI).asString())) { + if (!req.uri().equals(settingService.getSystem(MusicSetting.CONTROLLER_URI).asString())) { // 访问的路径不是 Web Socket 的端点地址,响应 404 ctx.channel().writeAndFlush(NFP).addListener(ChannelFutureListener.CLOSE); return; @@ -111,10 +111,10 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application }); pipeline.addLast(new WebSocketServerCompressionHandler()); pipeline.addLast(new WebSocketServerProtocolHandler( - settingService.getSystem(Setting.Module.Music.CONTROLLER_URI).asString(), + settingService.getSystem(MusicSetting.CONTROLLER_URI).asString(), null, true, - settingService.getSystem(Setting.Module.Music.MAX_FRAME_LENGTH).asLong() + settingService.getSystem(MusicSetting.MAX_FRAME_LENGTH).asLong() ) ); @@ -123,7 +123,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application } }); channel = bootstrap.bind().sync().channel(); - log.info("TimiMusicRC controller service startup with {}", settingService.getSystem(Setting.Module.Music.CONTROLLER_IP).asString()); + log.info("TimiMusicRC controller service startup with {}", settingService.getSystem(MusicSetting.CONTROLLER_IP).asString()); } catch (Exception e) { log.error("TimiMusicRC controller service startup error", e); } diff --git a/src/main/java/com/imyeyu/api/modules/music/runner/PlayerBootstrapRunner.java b/src/main/java/com/imyeyu/api/modules/music/runner/PlayerBootstrapRunner.java index 2f356ff..6804162 100644 --- a/src/main/java/com/imyeyu/api/modules/music/runner/PlayerBootstrapRunner.java +++ b/src/main/java/com/imyeyu/api/modules/music/runner/PlayerBootstrapRunner.java @@ -1,7 +1,7 @@ package com.imyeyu.api.modules.music.runner; -import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.service.SettingService; +import com.imyeyu.api.modules.music.bean.MusicSetting; import com.imyeyu.api.modules.music.handler.PlayerMessageHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; @@ -49,7 +49,7 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList } public void run(ApplicationArguments args) { - if (settingService.getSystem(Setting.Module.Music.ENABLE).isTrue()) { + if (settingService.getSystem(MusicSetting.ENABLE).isTrue()) { return; } ServerBootstrap bootstrap = new ServerBootstrap(); @@ -58,13 +58,13 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList try { bootstrap.group(boss, worker); bootstrap.channel(NioServerSocketChannel.class); - bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(Setting.Module.Music.PLAYER_PORT).asInteger())); + bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(MusicSetting.PLAYER_PORT).asInteger())); bootstrap.childHandler(new ChannelInitializer<>() { @Override protected void initChannel(Channel ch) { ch.pipeline().addLast(new LengthFieldBasedFrameDecoder( - settingService.getSystem(Setting.Module.Music.MAX_FRAME_LENGTH).asInteger(), + settingService.getSystem(MusicSetting.MAX_FRAME_LENGTH).asInteger(), 0, 4, 0, @@ -79,7 +79,7 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList channel = bootstrap.bind().sync().channel(); - log.info("TimiMusicRC player service startup with {}", settingService.getSystem(Setting.Module.Music.PLAYER_IP).asString()); + log.info("TimiMusicRC player service startup with {}", settingService.getSystem(MusicSetting.PLAYER_IP).asString()); } catch (Exception e) { log.error("TimiMusicRC player service startup error", e); } diff --git a/src/main/java/com/imyeyu/api/modules/system/bean/SystemStatusSetting.java b/src/main/java/com/imyeyu/api/modules/system/bean/SystemStatusSetting.java new file mode 100644 index 0000000..85f9275 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/bean/SystemStatusSetting.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.system.bean; + +import com.imyeyu.api.annotation.SettingBizType; +import com.imyeyu.api.bean.SettingEnum; +import com.imyeyu.api.modules.common.entity.Setting; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * + * + * @author 夜雨 + * @since 2026-04-24 14:23 + */ +@Getter +@SettingBizType("SYSTEM_STATUS") +@AllArgsConstructor +public enum SystemStatusSetting implements SettingEnum { + + /** 状态采集数据量 */ + LIMIT("512", Setting.ValueType.INTEGER), + + /** 状态采集网卡 */ + NETWORK_MAC(null, Setting.ValueType.STRING), + + /** 状态采集频率 */ + RATE("3000", Setting.ValueType.INTEGER); + + final String defaultValue; + + final Setting.ValueType valueType; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java index fb5c123..218c441 100644 --- a/src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java @@ -1,8 +1,8 @@ package com.imyeyu.api.modules.system.service.implement; -import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.service.SettingService; import com.imyeyu.api.modules.system.bean.ServerStatus; +import com.imyeyu.api.modules.system.bean.SystemStatusSetting; import com.imyeyu.api.modules.system.service.StatusService; import com.imyeyu.api.modules.system.vo.SystemStatusDataView; import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView; @@ -33,7 +33,7 @@ public class StatusServiceImplement implements StatusService { @Override public SystemStatusSnapshotView getStatus(String metrics) { long serverTime = Time.now(); - int sampleRateMs = settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger(); + int sampleRateMs = settingService.getSystem(SystemStatusSetting.RATE).asInteger(); EnumSet selectedMetrics = parseMetrics(metrics); synchronized (serverStatus) { @@ -48,7 +48,7 @@ public class StatusServiceImplement implements StatusService { @Override public SystemStatusHistoryView getStatusHistory(String window, String metrics) { long serverTime = Time.now(); - int sampleRateMs = settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger(); + int sampleRateMs = settingService.getSystem(SystemStatusSetting.RATE).asInteger(); EnumSet selectedMetrics = parseMetrics(metrics); synchronized (serverStatus) { @@ -261,7 +261,7 @@ public class StatusServiceImplement implements StatusService { */ private long parseWindowMs(String window, int sampleRateMs) { if (window == null || window.isBlank()) { - return (long) settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger() * sampleRateMs; + return (long) settingService.getSystem(SystemStatusSetting.RATE).asInteger() * sampleRateMs; } String normalized = window.trim().toLowerCase(Locale.ROOT); @@ -277,7 +277,7 @@ public class StatusServiceImplement implements StatusService { try { return Math.max(sampleRateMs, Long.parseLong(valueText) * unit); } catch (NumberFormatException e) { - return (long) settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger() * sampleRateMs; + return (long) settingService.getSystem(SystemStatusSetting.RATE).asInteger() * sampleRateMs; } } diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java index b77286e..2148ce0 100644 --- a/src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java @@ -1,6 +1,6 @@ package com.imyeyu.api.modules.system.task.status; -import com.imyeyu.api.modules.common.entity.Setting; +import com.imyeyu.api.modules.system.bean.SystemStatusSetting; import java.util.Deque; @@ -22,7 +22,7 @@ public abstract class AbstractDequeStatusCollector implements StatusCollector { */ protected void putDeque(StatusCollectContext context, Deque deque, T value) { deque.addLast(value); - if (context.getSettingService().getSystem(Setting.Module.SystemStatus.LIMIT).asInteger() < deque.size()) { + if (context.getSettingService().getSystem(SystemStatusSetting.LIMIT).asInteger() < deque.size()) { deque.pollFirst(); } } diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java index 24be298..abc847f 100644 --- a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java @@ -1,6 +1,6 @@ package com.imyeyu.api.modules.system.task.status.collector; -import com.imyeyu.api.modules.common.entity.Setting; +import com.imyeyu.api.modules.system.bean.SystemStatusSetting; import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector; import com.imyeyu.api.modules.system.task.status.StatusCollectContext; import lombok.extern.slf4j.Slf4j; @@ -24,7 +24,7 @@ public class NetworkStatusCollector extends AbstractDequeStatusCollector { @Override public void initialize(StatusCollectContext context) { List networkIFs = context.getHardware().getNetworkIFs(); - String targetMac = context.getSettingService().getSystem(Setting.Module.SystemStatus.NETWORK_MAC).asString(); + String targetMac = context.getSettingService().getSystem(SystemStatusSetting.NETWORK_MAC).asString(); for (NetworkIF networkIF : networkIFs) { if (networkIF.getMacaddr().equals(targetMac)) { networkIF.updateAttributes(); @@ -41,8 +41,8 @@ public class NetworkStatusCollector extends AbstractDequeStatusCollector { @Override public void collect(StatusCollectContext context) { - String targetMac = context.getSettingService().getSystem(Setting.Module.SystemStatus.NETWORK_MAC).asString(); - int sampleRateMs = context.getSettingService().getSystem(Setting.Module.SystemStatus.RATE).asInteger(); + String targetMac = context.getSettingService().getSystem(SystemStatusSetting.NETWORK_MAC).asString(); + int sampleRateMs = context.getSettingService().getSystem(SystemStatusSetting.RATE).asInteger(); for (NetworkIF networkIF : context.getHardware().getNetworkIFs()) { if (networkIF.getMacaddr().equals(targetMac)) { networkIF.updateAttributes(); diff --git a/src/main/java/com/imyeyu/api/modules/user/bean/UserSetting.java b/src/main/java/com/imyeyu/api/modules/user/bean/UserSetting.java new file mode 100644 index 0000000..dfe5688 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/bean/UserSetting.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.user.bean; + +import com.imyeyu.api.annotation.SettingBizType; +import com.imyeyu.api.bean.SettingEnum; +import com.imyeyu.api.modules.common.entity.Setting; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * + * + * @author 夜雨 + * @since 2026-04-24 15:03 + */ +@Getter +@SettingBizType("USER") +@AllArgsConstructor +public enum UserSetting implements SettingEnum { + + /** 启用登录 */ + ENABLE_LOGIN(Boolean.TRUE.toString(), Setting.ValueType.BOOLEAN), + + /** 启用注册 */ + ENABLE_REGISTER(Boolean.TRUE.toString(), Setting.ValueType.BOOLEAN), + + /** 允许数据更新 */ + ENABLE_PROFILE_UPDATE(Boolean.TRUE.toString(), Setting.ValueType.BOOLEAN), + + /** 令牌有效期 */ + TOKEN_TTL("7d", Setting.ValueType.TIME); + + final String defaultValue; + + final Setting.ValueType valueType; +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java index 9f84b4c..9066aa3 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java @@ -3,12 +3,12 @@ package com.imyeyu.api.modules.user.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.NotifyDetail; -import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.SettingService; import com.imyeyu.api.modules.common.service.SmsService; import com.imyeyu.api.modules.common.vo.SmsCaptchaVerifyRequest; import com.imyeyu.api.modules.user.bean.BootstrapData; +import com.imyeyu.api.modules.user.bean.UserSetting; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserRoleService; @@ -79,7 +79,7 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava { @Override public LoginResponse login(String token, String userId) { - Long time = settingService.getSystem(Setting.Module.User.TOKEN_TTL).asTime(); + Long time = settingService.getSystem(UserSetting.TOKEN_TTL).asTime(); long now = Time.now(); long expireAt = now + time; redisUserToken.set(getUserTokenKey(token), userId, expireAt - now); diff --git a/src/main/java/com/imyeyu/api/util/InitBuiltinSetting.java b/src/main/java/com/imyeyu/api/util/InitBuiltinSetting.java new file mode 100644 index 0000000..e8a8237 --- /dev/null +++ b/src/main/java/com/imyeyu/api/util/InitBuiltinSetting.java @@ -0,0 +1,82 @@ +package com.imyeyu.api.util; + +import com.imyeyu.api.bean.SettingEnum; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.entity.Setting; +import com.imyeyu.api.modules.common.service.SettingService; +import com.imyeyu.java.TimiJava; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/// 启动时初始化内建系统配置 +/// +/// @author 夜雨 +/// @since 2026-09-08 +@Component +@RequiredArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE) +public class InitBuiltinSetting implements ApplicationRunner { + + /// 设置枚举所在的扫描包 + private static final String SCAN_PACKAGE = "com.imyeyu.api"; + + private final SettingService settingService; + + /// 初始化所有设置枚举对应的系统配置 + /// + /// @param args 启动参数 + /// @throws Exception 初始化异常 + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public void run(ApplicationArguments args) throws Exception { + for (SettingEnum item : findSettingEnumList()) { + if (settingService.getSystem(item) == null) { + Setting setting = new Setting(); + setting.setBizType(item.getBizType()); + setting.setBizId(TimiJava.ZERO_UUID); + setting.setCode(item.name()); + setting.setValue(item.getDefaultValue()); + setting.setValueType(item.getValueType()); + setting.setIsPrivate(true); + settingService.create(setting); + } + } + } + + /// 扫描所有实现设置枚举接口的枚举常量 + /// + /// @return 设置枚举常量列表 + /// @throws ClassNotFoundException 枚举类无法加载 + private List findSettingEnumList() throws ClassNotFoundException { + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); + scanner.addIncludeFilter(new AssignableTypeFilter(SettingEnum.class)); + + List candidateList = new ArrayList<>(scanner.findCandidateComponents(SCAN_PACKAGE)); + List result = new ArrayList<>(); + for (BeanDefinition candidate : candidateList) { + String className = candidate.getBeanClassName(); + if (className == null) { + continue; + } + Class candidateClass = Class.forName(className); + if (!candidateClass.isEnum() || !SettingEnum.class.isAssignableFrom(candidateClass)) { + continue; + } + for (Object constant : candidateClass.getEnumConstants()) { + result.add((SettingEnum) constant); + } + } + return result; + } +} From 818207d6c9d1f2dc4bb95ecc5f3a902d488e9e6d Mon Sep 17 00:00:00 2001 From: Timi Date: Tue, 8 Sep 2026 18:09:31 +0800 Subject: [PATCH 2/5] fix FileController --- .../api/modules/system/bean/ServerFile.java | 2 +- .../system/bean/SystemFileSetting.java | 21 +++++++++++++++++++ .../system/controller/FileController.java | 13 ++++-------- .../implement/FileServiceImplement.java | 5 ++--- 4 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/imyeyu/api/modules/system/bean/SystemFileSetting.java diff --git a/src/main/java/com/imyeyu/api/modules/system/bean/ServerFile.java b/src/main/java/com/imyeyu/api/modules/system/bean/ServerFile.java index baf4295..61b600b 100644 --- a/src/main/java/com/imyeyu/api/modules/system/bean/ServerFile.java +++ b/src/main/java/com/imyeyu/api/modules/system/bean/ServerFile.java @@ -61,7 +61,7 @@ public class ServerFile implements Serializable { name = file.getName(); absolutePath = file.getAbsolutePath().replaceAll("\\\\", "/"); SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class); - absolutePath = absolutePath.substring("E:\\IDEAProject\\TimiServerAPI\\data\\file".length()); + absolutePath = absolutePath.substring(new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString()).getAbsolutePath().length()); modifiedAt = file.lastModified(); this.file = file.isFile(); diff --git a/src/main/java/com/imyeyu/api/modules/system/bean/SystemFileSetting.java b/src/main/java/com/imyeyu/api/modules/system/bean/SystemFileSetting.java new file mode 100644 index 0000000..768ae41 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/bean/SystemFileSetting.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.system.bean; + +import com.imyeyu.api.bean.SettingEnum; +import com.imyeyu.api.modules.common.entity.Setting; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/// +/// +/// @author 夜雨 +/// @since 2026-09-08 17:08 +@Getter +@AllArgsConstructor +public enum SystemFileSetting implements SettingEnum { + + BASE_PATH("data/file", Setting.ValueType.STRING); + + final String defaultValue; + + final Setting.ValueType valueType; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/controller/FileController.java b/src/main/java/com/imyeyu/api/modules/system/controller/FileController.java index a6da743..e6ccf8d 100644 --- a/src/main/java/com/imyeyu/api/modules/system/controller/FileController.java +++ b/src/main/java/com/imyeyu/api/modules/system/controller/FileController.java @@ -1,9 +1,8 @@ package com.imyeyu.api.modules.system.controller; -import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.service.SettingService; -import com.imyeyu.api.modules.common.service.TagService; import com.imyeyu.api.modules.system.bean.ServerFile; +import com.imyeyu.api.modules.system.bean.SystemFileSetting; import com.imyeyu.api.modules.system.bean.TransferFile; import com.imyeyu.api.modules.system.service.AsyncTaskService; import com.imyeyu.api.modules.system.service.FileService; @@ -47,7 +46,6 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.RandomAccessFile; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -67,7 +65,6 @@ import java.util.Map; @RequestMapping("/system/file") public class FileController implements TimiJava, OS.FileSystem { - private final TagService tagService; private final FileService service; private final SettingService settingService; private final AsyncTaskService asyncTaskService; @@ -143,7 +140,7 @@ public class FileController implements TimiJava, OS.FileSystem { public void read(HttpServletRequest req, HttpServletResponse resp) { try { String path = req.getServletPath().substring("/system/file/read".length()); - path = "/root/bin/data/mnt" + path; + path = settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path; if (TimiJava.isEmpty(path)) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; @@ -195,8 +192,7 @@ public class FileController implements TimiJava, OS.FileSystem { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } -// path = settingService.getAsString(Setting.Module.System.FILE_BASE_PATH) + path; - File file = new File(path); + File file = new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path); if (!file.exists()) { log.warn("preview a not exist file: {}", path); resp.setStatus(HttpServletResponse.SC_NOT_FOUND); @@ -409,8 +405,7 @@ public class FileController implements TimiJava, OS.FileSystem { public void download(HttpServletResponse resp) { try { String path = TimiSpring.cutURIStartAt("/system/file/download"); - path = "E:/IDEAProject/TimiServerAPI/data/file" + path; - File file = new File(path); + File file = new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path); service.checkAccessPermission(file.getAbsolutePath()); String mimeType = new Tika().detect(file); diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/FileServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/FileServiceImplement.java index 820276d..4eb16ac 100644 --- a/src/main/java/com/imyeyu/api/modules/system/service/implement/FileServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/FileServiceImplement.java @@ -1,8 +1,8 @@ package com.imyeyu.api.modules.system.service.implement; -import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.service.SettingService; import com.imyeyu.api.modules.system.bean.ServerFile; +import com.imyeyu.api.modules.system.bean.SystemFileSetting; import com.imyeyu.api.modules.system.bean.TransferFile; import com.imyeyu.api.modules.system.service.AsyncTaskService; import com.imyeyu.api.modules.system.service.FileService; @@ -257,8 +257,7 @@ public class FileServiceImplement implements TimiJava, FileService { } private String getPath(String path) { -// String basePath = settingService.getSystem(Setting.Module.System.FILE_BASE_PATH).asString(); - String basePath = "E:\\IDEAProject\\TimiServerAPI\\data\\file"; + String basePath = settingService.getSystem(SystemFileSetting.BASE_PATH).asString(); if (TimiJava.isEmpty(path)) { return basePath; } From ea44dd5c0e406a800034cc0a6a4b3a4dc3bf2c66 Mon Sep 17 00:00:00 2001 From: Timi Date: Wed, 9 Sep 2026 01:08:45 +0800 Subject: [PATCH 3/5] update point rule groovy script --- .../common/controller/NotifyController.java | 8 +++++ .../modules/common/entity/GroovyScript.java | 4 +++ .../mapper/GroovyScriptVersionMapper.java | 4 +++ .../common/service/GroovyScriptService.java | 29 ++++++------------- .../GroovyScriptServiceImplement.java | 24 +++++++++------ .../implement/NotifyServiceImplement.java | 10 +++---- .../controller/GaoPointRuleController.java | 16 ++++++++++ .../api/modules/gao/entity/GaoPointRule.java | 5 ++++ ...__remove_gao_point_rule_legacy_columns.sql | 9 ++++++ 9 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 src/main/resources/db/migration/timiserver/V73__remove_gao_point_rule_legacy_columns.sql diff --git a/src/main/java/com/imyeyu/api/modules/common/controller/NotifyController.java b/src/main/java/com/imyeyu/api/modules/common/controller/NotifyController.java index 605e2f8..d3c7b07 100644 --- a/src/main/java/com/imyeyu/api/modules/common/controller/NotifyController.java +++ b/src/main/java/com/imyeyu/api/modules/common/controller/NotifyController.java @@ -58,6 +58,14 @@ public class NotifyController { @RequiredToken @PostMapping("/create") public Notify create(@RequestBody Notify notify) { + String sendFrom = userLoginService.isLogged() ? userLoginService.getRequireLoginUserId() : null; + notify.setCreatedBy(sendFrom); + for (NotifyDetail detail : notify.getDetailList()) { + detail.setNotifyId(notify.getId()); + if (TimiJava.isNotEmpty(sendFrom)) { + detail.setSendFrom(sendFrom); + } + } service.create(notify); return notify; } diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java index a6c3d02..58a9619 100644 --- a/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java +++ b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.common.entity; +import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; import lombok.EqualsAndHashCode; @@ -39,4 +40,7 @@ public class GroovyScript extends UUIDEntity { /// 当前发布版本 ID private String publishedVersionId; + + @Transient + private GroovyScriptVersion latest; } diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java index f563e21..033fe58 100644 --- a/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java @@ -2,10 +2,14 @@ package com.imyeyu.api.modules.common.mapper; import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; /// Groovy 脚本版本 Mapper /// /// @author Codex /// @since 2026-09-04 public interface GroovyScriptVersionMapper extends BaseMapper { + + @Select("SELECT * FROM `groovy_script_version` WHERE `script_id` = #{scriptId} " + NOT_DELETE + " ORDER BY `version` DESC " + LIMIT_1) + GroovyScriptVersion selectLatest(String scriptId); } diff --git a/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java b/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java index eee5ef4..7d59465 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java @@ -1,8 +1,8 @@ package com.imyeyu.api.modules.common.service; +import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult; import com.imyeyu.api.modules.common.entity.GroovyScript; import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; -import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult; import com.imyeyu.spring.service.BaseService; import java.util.List; @@ -16,6 +16,12 @@ import java.util.Map; /// @since 2026-09-04 public interface GroovyScriptService extends BaseService { + /// 按业务归属查询脚本列表 + /// + /// @param bizType 业务归属 + /// @return 脚本列表 + List listByBizType(GroovyScript.BizType bizType); + /// 按业务归属和代码查询脚本 /// /// @param bizType 业务归属 @@ -23,26 +29,14 @@ public interface GroovyScriptService extends BaseService { /// @return 脚本,不存在时返回 null GroovyScript getByCode(GroovyScript.BizType bizType, String code); - /// 创建脚本元数据 - /// - /// 新建脚本默认停用,且没有已发布版本 - /// - /// @param script 脚本元数据 - void create(GroovyScript script); - - /// 更新脚本元数据 - /// - /// 业务归属和代码是脚本稳定标识,创建后不可变更 - /// - /// @param script 脚本元数据 - void update(GroovyScript script); - /// 设置脚本启用状态 /// /// @param id 脚本 ID /// @param enabled true 为允许执行已发布版本 void setEnabled(String id, boolean enabled); + GroovyScriptVersion getLatestVersion(String scriptId); + /// 创建不可变的脚本版本 /// /// @param scriptId 脚本 ID @@ -67,11 +61,6 @@ public interface GroovyScriptService extends BaseService { /// @param scriptId 脚本 ID void unpublish(String scriptId); - /// 删除脚本 - /// - /// @param id 脚本 ID - void delete(String id); - /// 执行已发布且启用的脚本 /// /// @param bizType 业务归属 diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java index 890571c..f90ee3f 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java @@ -1,9 +1,9 @@ package com.imyeyu.api.modules.common.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult; import com.imyeyu.api.modules.common.entity.GroovyScript; import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; -import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult; import com.imyeyu.api.modules.common.mapper.GroovyScriptMapper; import com.imyeyu.api.modules.common.mapper.GroovyScriptVersionMapper; import com.imyeyu.api.modules.common.service.GroovyScriptService; @@ -52,6 +52,14 @@ public class GroovyScriptServiceImplement extends AbstractEntityService listByBizType(GroovyScript.BizType bizType) { + TimiException.required(bizType, "未找到脚本业务归属"); + GroovyScript example = new GroovyScript(); + example.setBizType(bizType); + return mapper.selectAllByExample(example); + } + @Override public GroovyScript getByCode(GroovyScript.BizType bizType, String code) { GroovyScript example = new GroovyScript(); @@ -95,6 +103,11 @@ public class GroovyScriptServiceImplement extends AbstractEntityService scriptList() { + return groovyScriptService.listByBizType(GroovyScript.BizType.GAO_POINT_RULE); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE) diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java index eba636a..a60ec2c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java @@ -1,5 +1,7 @@ package com.imyeyu.api.modules.gao.entity; +import com.imyeyu.api.modules.common.entity.GroovyScript; +import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; import lombok.EqualsAndHashCode; @@ -96,4 +98,7 @@ public class GaoPointRule extends UUIDEntity { /// 备注 private String remark; + + @Transient + private GroovyScript groovyScript; } diff --git a/src/main/resources/db/migration/timiserver/V73__remove_gao_point_rule_legacy_columns.sql b/src/main/resources/db/migration/timiserver/V73__remove_gao_point_rule_legacy_columns.sql new file mode 100644 index 0000000..93bd127 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V73__remove_gao_point_rule_legacy_columns.sql @@ -0,0 +1,9 @@ +-- 积分规则已迁移到 trigger_source_type、trigger_ref_type、trigger_ref_id 模型。 +-- 删除旧模型字段,避免实体未赋值时触发非空约束。 +ALTER TABLE `gao_point_rule` + DROP KEY `idx_gao_point_rule_match`, + DROP KEY `idx_gao_point_rule_type`, + DROP COLUMN `event_id`, + DROP COLUMN `trigger_type`, + DROP COLUMN `point_mode`, + DROP COLUMN `condition_json`; From 00bb3007556c645489b106c16c98ad097ef45150 Mon Sep 17 00:00:00 2001 From: Timi Date: Wed, 9 Sep 2026 01:49:40 +0800 Subject: [PATCH 4/5] support split fulfillment by base unit --- .../gao/entity/GaoOrderFulfillmentItem.java | 18 +- .../modules/gao/entity/GaoSalesOrderItem.java | 23 ++- .../gao/mapper/GaoSalesOrderItemMapper.java | 8 +- .../gao/service/GaoSalesOrderService.java | 8 +- .../GaoCustomerPackageServiceImplement.java | 115 +++++++---- .../GaoOrderFulfillmentServiceImplement.java | 183 +++++++++++------- .../GaoSalesOrderServiceImplement.java | 64 ++++-- .../vo/GaoOrderFulfillmentItemRequest.java | 3 + ...ort_gao_split_fulfillment_by_base_unit.sql | 23 +++ .../timi-server/gao/GaoProductChartMapper.xml | 16 +- .../gao/GaoProductSkuChartMapper.xml | 6 +- .../gao/GaoSalesOrderChartMapper.xml | 26 +-- .../gao/GaoSalesOrderItemMapper.xml | 16 +- 13 files changed, 320 insertions(+), 189 deletions(-) create mode 100644 src/main/resources/db/migration/timiserver/V74__support_gao_split_fulfillment_by_base_unit.sql diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java index 645efbf..008dae5 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java @@ -29,7 +29,7 @@ public class GaoOrderFulfillmentItem implements IDEntity, Creatable { /// SKU ID private String skuId; - /// SKU 单位 ID + /// 本次实际履约 SKU 单位 ID private String skuUnitId; /// 产品图片附件 @@ -45,17 +45,17 @@ public class GaoOrderFulfillmentItem implements IDEntity, Creatable { /// 基础单位数量 private BigDecimal baseQuantity; - /// 操作前待履约数量 - private BigDecimal reservedBefore; + /// 操作前待履约基础单位数量 + private BigDecimal reservedBaseBefore; - /// 操作后待履约数量 - private BigDecimal reservedAfter; + /// 操作后待履约基础单位数量 + private BigDecimal reservedBaseAfter; - /// 操作前已履约数量 - private BigDecimal fulfilledBefore; + /// 操作前已履约基础单位数量 + private BigDecimal fulfilledBaseBefore; - /// 操作后已履约数量 - private BigDecimal fulfilledAfter; + /// 操作后已履约基础单位数量 + private BigDecimal fulfilledBaseAfter; /// 产品名称快照 private String productNameSnapshot; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java index db2166d..eb66708 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java @@ -71,11 +71,14 @@ public class GaoSalesOrderItem extends UUIDEntity { /// 购买数量 private BigDecimal quantity; - /// 待履约数量 - private BigDecimal reservedQuantity; + /// 购买基础单位数量 + private BigDecimal baseQuantity; - /// 已履约数量 - private BigDecimal fulfilledQuantity; + /// 待履约基础单位数量 + private BigDecimal reservedBaseQuantity; + + /// 已履约基础单位数量 + private BigDecimal fulfilledBaseQuantity; /// 实际单价,单位分 private Long unitPrice; @@ -89,15 +92,15 @@ public class GaoSalesOrderItem extends UUIDEntity { /// 备注 private String remark; - /// 待核销数量 + /// 待核销基础单位数量 @Transient - private BigDecimal pendingQuantity; + private BigDecimal pendingBaseQuantity; - /// 当前可履约数量 + /// 当前可履约基础单位数量 @Transient - private BigDecimal availableQuantity; + private BigDecimal availableBaseQuantity; - /// 权益剩余数量 + /// 权益剩余基础单位数量 @Transient - private BigDecimal remainingQuantity; + private BigDecimal remainingBaseQuantity; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java index a51eb31..484ad80 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java @@ -22,9 +22,9 @@ public interface GaoSalesOrderItemMapper extends BaseMapper /// 检查履约权限 void checkFulfillmentPermission(GaoSalesOrder order); - /// 原子调整订单项待履约数量 - int updateItemReservedQuantity(String itemId, BigDecimal delta); + /// 原子调整订单项待履约基础单位数量 + int updateItemReservedBaseQuantity(String itemId, BigDecimal delta); - /// 原子调整订单项已履约数量 - int updateItemFulfilledQuantity(String itemId, BigDecimal delta); + /// 原子调整订单项已履约基础单位数量 + int updateItemFulfilledBaseQuantity(String itemId, BigDecimal delta); /// 锁定订单明细 GaoSalesOrderItem getItemForUpdate(String itemId); 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 index d63d3e9..6e75900 100644 --- 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 @@ -22,6 +22,7 @@ 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.gao.vo.GaoPackageOrderCreateData; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; @@ -38,6 +39,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -53,6 +58,8 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class GaoCustomerPackageServiceImplement extends AbstractEntityService implements GaoCustomerPackageService { + private static final BigDecimal ZERO = BigDecimal.ZERO; + private final GaoCustomerPackageMapper mapper; private final GaoPackageConfigService packageConfigService; private final GaoProductSkuService skuService; @@ -143,9 +150,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList = new java.util.ArrayList<>(request.getItemList()); - verifyPurchaseItems(itemList); - itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort)); + List requestItemList = new ArrayList<>(request.getItemList()); + verifyPurchaseItems(requestItemList); + boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO, + com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue()); + List itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList, canOverridePriceAndQuantity); + itemList.sort(Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort).thenComparing(GaoCustomerPackagePurchaseItem::getSkuId) + .thenComparing(GaoCustomerPackagePurchaseItem::getSkuUnitId)); request.setItemList(itemList); GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo()); @@ -154,19 +165,14 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService orderItemList = itemList.stream().map(this::toOrderItem).toList(); @@ -267,8 +273,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList) { - TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品"); Set keySet = new HashSet<>(); for (int i = 0; i < itemList.size(); i++) { GaoCustomerPackagePurchaseItem item = itemList.get(i); @@ -285,7 +291,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数"); String key = item.getSkuId() + "|" + item.getSkuUnitId(); TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复"); @@ -293,38 +299,69 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList) { - for (GaoPackageConfigItem item : itemList) { - TimiException.required(item.getSkuId(), "套餐配置缺少 SKU"); - TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位"); - // 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态 - skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId()); + private List resolvePurchaseItems(String storeId, List requestItemList, + List configItemList, boolean canOverridePriceAndQuantity) { + Map> configItemMap = configItemList.stream() + .collect(Collectors.groupingBy(GaoPackageConfigItem::getSkuId)); + Map configBaseQuantityMap = new HashMap<>(); + for (GaoPackageConfigItem configItem : configItemList) { + configBaseQuantityMap.merge(configItem.getSkuId(), + toBaseQuantity(configItem.getDefaultQuantity(), configItem.getBaseQuantityPerUnitSnapshot(), "套餐配置基础数量最多保留三位小数"), BigDecimal::add); } + Map requestBaseQuantityMap = new HashMap<>(); + List result = new ArrayList<>(); + for (GaoCustomerPackagePurchaseItem requestItem : requestItemList) { + List skuConfigItemList = configItemMap.get(requestItem.getSkuId()); + TimiException.requiredTrue(skuConfigItemList != null && !skuConfigItemList.isEmpty(), "购买 SKU 不属于套餐配置"); + GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, requestItem.getSkuId(), requestItem.getSkuUnitId()); + TimiException.requiredTrue(skuConfigItemList.stream().anyMatch(configItem -> 0 <= configItem.getBaseQuantityPerUnitSnapshot() + .compareTo(selection.getUnit().getBaseQuantityPerUnit())), "套餐权益单位不能高于配置单位"); + BigDecimal requestBaseQuantity = toBaseQuantity(requestItem.getTotalQuantity(), selection.getUnit().getBaseQuantityPerUnit(), "套餐权益基础数量最多保留三位小数"); + requestBaseQuantityMap.merge(requestItem.getSkuId(), requestBaseQuantity, BigDecimal::add); + if (requestItem.getTotalQuantity().signum() == 0) { + continue; + } + if (!Boolean.TRUE.equals(selection.getSku().getAllowDecimalQuantity())) { + TimiException.requiredTrue(requestItem.getTotalQuantity().stripTrailingZeros().scale() <= 0, "该 SKU 不允许小数数量"); + } + GaoPackageConfigItem template = resolveItemTemplate(skuConfigItemList, selection.getUnit().getId()); + GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem(); + item.setSkuId(selection.getSku().getId()); + item.setSkuUnitId(selection.getUnit().getId()); + item.setProductNameSnapshot(selection.getProduct().getName()); + item.setProductIdSnapshot(selection.getProduct().getId()); + item.setSkuCodeSnapshot(selection.getSku().getCode()); + item.setSkuNameSnapshot(selection.getSku().getName()); + item.setSpecSummarySnapshot(selection.getSku().getSpecSummary()); + item.setUnitSnapshot(selection.getUnit().getName()); + item.setBaseQuantityPerUnitSnapshot(selection.getUnit().getBaseQuantityPerUnit()); + item.setTotalQuantity(requestItem.getTotalQuantity()); + item.setSort(template.getSort()); + item.setRemark(template.getRemark()); + result.add(item); + } + TimiException.requiredTrue(!result.isEmpty(), "购买套餐至少需要一个产品"); + if (!canOverridePriceAndQuantity) { + for (Map.Entry entry : configBaseQuantityMap.entrySet()) { + TimiException.requiredTrue(entry.getValue().compareTo(requestBaseQuantityMap.getOrDefault(entry.getKey(), ZERO)) == 0, + "普通员工不能修改套餐权益配额"); + } + } + return result; } - private void verifySameStructureAsConfig(List itemList, List configItemList) { - TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致"); - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); - for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId()); - TimiException.required(item, "套餐权益项目必须与套餐配置一致"); - item.setProductNameSnapshot(configItem.getProductNameSnapshot()); - item.setProductIdSnapshot(configItem.getProductIdSnapshot()); - item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot()); - item.setSkuNameSnapshot(configItem.getSkuNameSnapshot()); - item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot()); - item.setUnitSnapshot(configItem.getUnitSnapshot()); - item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); - TimiException.requiredTrue(item.getSort().equals(configItem.getSort()) - && sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改"); - } + private GaoPackageConfigItem resolveItemTemplate(List configItemList, String skuUnitId) { + return configItemList.stream().filter(item -> skuUnitId.equals(item.getSkuUnitId())).findFirst() + .orElseGet(() -> configItemList.stream().min(Comparator.comparing(GaoPackageConfigItem::getSort)).orElseThrow()); } - private void verifySameQuantityAsConfig(List itemList, List configItemList) { - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); - for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId()); - TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额"); + private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) { + BigDecimal result = quantity.multiply(baseQuantityPerUnit); + TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message); + try { + return result.setScale(3, RoundingMode.UNNECESSARY); + } catch (ArithmeticException e) { + throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, message, e); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java index 631c9dc..ce884f8 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java @@ -29,6 +29,7 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest; import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; @@ -46,6 +47,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -194,12 +196,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService sourceList = loadSources(order, itemRequests(snapshot.getId()), false); + List sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId())); GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); storeScopeService.checkStoreId(fulfillment.getStoreId()); TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态"); TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存"); - salesOrderService.checkFulfillmentPermission(order); List fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); List stockItems = buildStockItems(order.getStoreId(), sourceList); @@ -228,12 +229,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService sourceList = loadSources(order, itemRequests(snapshot.getId()), false); + List sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId())); GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); storeScopeService.checkStoreId(fulfillment.getStoreId()); TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态"); TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约"); - salesOrderService.checkFulfillmentPermission(order); List fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); assertReserved(sourceList); @@ -283,7 +283,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService sourceList = loadSources(order, itemRequests(snapshot.getId()), false); + List sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId())); GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); storeScopeService.checkStoreId(fulfillment.getStoreId()); TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK @@ -326,7 +326,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService sourceItemList = itemMapper.selectByFulfillmentId(sourceSnapshot.getId()); - List sourceList = loadSources(order, copyRequestItems(sourceItemList), false); + List sourceList = loadPersistedSources(order, sourceItemList); GaoOrderFulfillment source = lockFulfillment(sourceSnapshot.getId(), order); storeScopeService.checkStoreId(source.getStoreId()); TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正"); @@ -356,10 +356,10 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService sourceList) { for (SourceEntry source : sourceList) { - TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "订单明细占用失败"); + TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细占用失败"); GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId()); itemMapper.insert(item); } } private void decreaseReserved(SourceEntry source) { - TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "订单明细占用释放失败"); + TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细占用释放失败"); } private void increaseFulfilled(SourceEntry source) { - TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "订单明细履约失败"); + TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细履约失败"); } private void decreaseFulfilled(SourceEntry source) { - TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正"); - TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败"); + TimiException.requiredTrue(0 <= source.fulfilledBaseBefore().compareTo(source.baseQuantity()), "已履约数量不足以冲正"); + TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细冲正失败"); } private void assertReserved(List sourceList) { for (SourceEntry source : sourceList) { - TimiException.requiredTrue(0 <= source.reservedBefore().compareTo(source.quantity()), "履约待占用数量不足"); + TimiException.requiredTrue(0 <= source.reservedBaseBefore().compareTo(source.baseQuantity()), "履约待占用数量不足"); } } @@ -430,6 +430,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService loadPersistedSources(GaoSalesOrder order, List itemList) { + TimiException.requiredTrue(itemList != null && !itemList.isEmpty(), "履约单没有明细"); + List sortedItemList = new ArrayList<>(itemList); + sortedItemList.sort(Comparator.comparing(GaoOrderFulfillmentItem::getSourceOrderItemId)); + Set sourceIdSet = new HashSet<>(); + List result = new ArrayList<>(); + for (GaoOrderFulfillmentItem item : sortedItemList) { + TimiException.required(item, "履约明细不能为空"); + String sourceId = requiredText(item.getSourceOrderItemId(), "履约明细来源订单项不能为空"); + TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细"); + GaoSalesOrderItem source = salesOrderService.getItemForUpdate(sourceId); + validateSourceOrderItem(order, source); + GaoProductSku sku = skuService.get(source.getSkuId()); + TimiException.required(sku, "履约来源 SKU 不存在"); + GaoProduct product = productService.get(sku.getProductId()); + TimiException.required(product, "履约来源产品不存在"); + TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店"); + BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(item.getBaseQuantityPerUnitSnapshot()); + BigDecimal quantity = requiredPositiveQuantity(item.getQuantity(), "履约数量"); + BigDecimal baseQuantity = requiredBaseQuantity(item.getBaseQuantity()); + TimiException.requiredTrue(baseQuantity.compareTo(toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数")) == 0, + "履约基础数量与单位换算快照不一致"); + result.add(buildSource(order, source, item.getSkuUnitId(), item.getUnitSnapshot(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, false)); + } + return result; } - private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String skuId, String unitId, String productId, - String productName, String skuCode, String skuName, String specSummary, JsonNode skuSpecSnapshot, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity, - BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit, - boolean checkAvailable) { + private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, GaoProduct product, GaoProductSku sku, + GaoProductSkuUnit unit, BigDecimal quantity, boolean checkAvailable) { + TimiException.requiredTrue(source.getSkuId().equals(sku.getId()), "履约单位不属于来源 SKU"); + TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店"); + quantity = requiredPositiveQuantity(quantity, "履约数量"); if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); } - BigDecimal reservedBefore = decimal(reservedQuantity); - BigDecimal fulfilledBefore = decimal(fulfilledQuantity); + BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(unit.getBaseQuantityPerUnit()); + BigDecimal baseQuantity = toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数"); + return buildSource(order, source, unit.getId(), unit.getName(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, checkAvailable); + } + + private SourceEntry buildSource(GaoSalesOrder order, GaoSalesOrderItem source, String unitId, String unitName, + BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal baseQuantity, GaoProduct product, GaoProductSku sku, + boolean checkAvailable) { + BigDecimal reservedBaseBefore = decimal(source.getReservedBaseQuantity()); + BigDecimal fulfilledBaseBefore = decimal(source.getFulfilledBaseQuantity()); + BigDecimal totalBaseQuantity = requiredBaseQuantity(source.getBaseQuantity()); if (checkAvailable) { - BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore); - TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足"); + BigDecimal availableBaseQuantity = totalBaseQuantity.subtract(reservedBaseBefore).subtract(fulfilledBaseBefore); + TimiException.requiredTrue(0 <= availableBaseQuantity.compareTo(baseQuantity), "来源产品可履约数量不足"); } - return new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, skuSpecSnapshot, unitName, baseQuantityPerUnit, - quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit); + return new SourceEntry(source.getId(), source.getSkuId(), unitId, source.getProductIdSnapshot(), source.getProductNameSnapshot(), + source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), source.getSkuSpecSnapshot(), unitName, + baseQuantityPerUnit, quantity, baseQuantity, totalBaseQuantity, reservedBaseBefore, fulfilledBaseBefore, product, sku); + } + + private void validateSourceOrderItem(GaoSalesOrder order, GaoSalesOrderItem source) { + TimiException.required(source, "not found sales order item"); + TimiException.requiredTrue(order.getId().equals(source.getSalesOrderId()), "订单明细不属于当前订单"); + GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE + ? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT; + TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配"); } private List buildStockItems(String storeId, List sourceList) { @@ -489,7 +523,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null); TimiException.required(baseUnit, "实物产品缺少基础库存单位"); - BigDecimal baseQuantity = source.quantity().multiply(source.unit().getBaseQuantityPerUnit()); + BigDecimal baseQuantity = source.baseQuantity(); GaoStockChangeItem item = itemMap.get(source.sku().getId()); if (item == null) { item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity); @@ -528,21 +562,6 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService itemRequests(String fulfillmentId) { - return copyRequestItems(itemMapper.selectByFulfillmentId(fulfillmentId)); - } - - private List copyRequestItems(List itemList) { - List result = new ArrayList<>(); - for (GaoOrderFulfillmentItem item : TimiJava.safeIterable(itemList)) { - GaoOrderFulfillmentItemRequest request = new GaoOrderFulfillmentItemRequest(); - request.setSourceOrderItemId(item.getSourceOrderItemId()); - request.setQuantity(item.getQuantity()); - result.add(request); - } - return result; - } - private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) { GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); result.setSkuId(source.getSkuId()); @@ -561,20 +580,36 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService= value.stripTrailingZeros().scale(), fieldName + "最多保留三位小数"); + return value; + } + + private BigDecimal requiredBaseQuantity(BigDecimal value) { + TimiException.required(value, "基础单位数量不能为空"); + TimiException.requiredTrue(0 < value.signum(), "基础单位数量必须大于 0"); + TimiException.requiredTrue(3 >= value.stripTrailingZeros().scale(), "基础单位数量最多保留三位小数"); + return value; + } + + private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) { + BigDecimal result = quantity.multiply(baseQuantityPerUnit); + TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message); + try { + return result.setScale(3, RoundingMode.UNNECESSARY); + } catch (ArithmeticException e) { + throw new TimiException(TimiCode.ERROR, message, e); + } + } + private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) { TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId()) && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) @@ -582,12 +617,14 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService existingItems = itemMapper.selectByFulfillmentId(existing.getId()); List requestItems = request.getItemList(); TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致"); - Map requestMap = new HashMap<>(); + Map requestMap = new HashMap<>(); for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) { - requestMap.put(item.getSourceOrderItemId(), item.getQuantity()); + requestMap.put(item.getSourceOrderItemId(), item); } for (GaoOrderFulfillmentItem item : existingItems) { - TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getSourceOrderItemId(), ZERO)) == 0, "requestNo 对应履约请求内容不一致"); + GaoOrderFulfillmentItemRequest requestItem = requestMap.get(item.getSourceOrderItemId()); + TimiException.requiredTrue(requestItem != null && item.getSkuUnitId().equals(requestItem.getSkuUnitId()) + && item.getQuantity().compareTo(requestItem.getQuantity()) == 0, "requestNo 对应履约请求内容不一致"); } } @@ -671,8 +708,8 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService itemList = itemMapper.selectBySalesOrderId(order.getId()); boolean fulfilled = !itemList.isEmpty(); for (GaoSalesOrderItem item : itemList) { - if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) { + if (decimal(item.getFulfilledBaseQuantity()).compareTo(decimal(item.getBaseQuantity())) < 0) { fulfilled = false; break; } @@ -383,8 +389,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数"); TimiException.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空"); TimiException.requiredTrue(0 < item.getBaseQuantityPerUnitSnapshot().signum(), "订单项单位换算快照必须大于 0"); - TimiException.requiredTrue(item.getReservedQuantity() != null && item.getFulfilledQuantity() != null, + TimiException.required(item.getBaseQuantity(), "订单项基础单位数量不能为空"); + TimiException.requiredTrue(item.getBaseQuantity().compareTo(toBaseQuantity(item.getQuantity(), item.getBaseQuantityPerUnitSnapshot(), "订单项基础单位数量最多保留三位小数")) == 0, + "订单项基础单位数量与单位换算快照不一致"); + TimiException.requiredTrue(item.getReservedBaseQuantity() != null && item.getFulfilledBaseQuantity() != null, "订单项履约数量不能为空"); - TimiException.requiredTrue(0 <= item.getReservedQuantity().signum() && 0 <= item.getFulfilledQuantity().signum(), + TimiException.requiredTrue(0 <= item.getReservedBaseQuantity().signum() && 0 <= item.getFulfilledBaseQuantity().signum(), "订单项履约数量不能为负数"); - TimiException.requiredTrue(item.getReservedQuantity().add(item.getFulfilledQuantity()).compareTo(item.getQuantity()) <= 0, + TimiException.requiredTrue(item.getReservedBaseQuantity().add(item.getFulfilledBaseQuantity()).compareTo(item.getBaseQuantity()) <= 0, "订单项履约数量不能超过购买数量"); if (expectedItemType == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT) { TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价"); @@ -544,12 +554,12 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService itemList) { @@ -606,6 +616,24 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService= result.stripTrailingZeros().scale(), message); + try { + return result.setScale(3, RoundingMode.UNNECESSARY); + } catch (ArithmeticException e) { + throw new TimiException(TimiCode.ERROR, message, e); + } + } + private long toCentAmount(BigDecimal quantity, long unitPrice) { try { return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact(); diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java index b21a153..b0b6b36 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java @@ -14,6 +14,9 @@ public class GaoOrderFulfillmentItemRequest { /// 来源销售订单明细 ID private String sourceOrderItemId; + /// 本次实际履约 SKU 单位 ID + private String skuUnitId; + /// 本次履约数量 private BigDecimal quantity; } diff --git a/src/main/resources/db/migration/timiserver/V74__support_gao_split_fulfillment_by_base_unit.sql b/src/main/resources/db/migration/timiserver/V74__support_gao_split_fulfillment_by_base_unit.sql new file mode 100644 index 0000000..e2167de --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V74__support_gao_split_fulfillment_by_base_unit.sql @@ -0,0 +1,23 @@ +-- 销售订单明细的履约计数统一改为基础单位,订单下单单位和数量仍保留用于展示 +ALTER TABLE `gao_sales_order_item` + ADD COLUMN `base_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '购买基础单位数量' AFTER `quantity`, + ADD COLUMN `reserved_base_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '待履约基础单位数量' AFTER `base_quantity`, + ADD COLUMN `fulfilled_base_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已履约基础单位数量' AFTER `reserved_base_quantity`; + +UPDATE `gao_sales_order_item` +SET `base_quantity` = `quantity` * `base_quantity_per_unit_snapshot`, + `reserved_base_quantity` = `reserved_quantity` * `base_quantity_per_unit_snapshot`, + `fulfilled_base_quantity` = `fulfilled_quantity` * `base_quantity_per_unit_snapshot`; + +ALTER TABLE `gao_sales_order_item` + DROP COLUMN `reserved_quantity`, + DROP COLUMN `fulfilled_quantity`; + +-- 履约明细的前后值改名,明确其计量单位为基础单位 +ALTER TABLE `gao_order_fulfillment_item` + CHANGE COLUMN `reserved_before` `reserved_base_before` DECIMAL(18,3) NOT NULL COMMENT '操作前待履约基础单位数量', + CHANGE COLUMN `reserved_after` `reserved_base_after` DECIMAL(18,3) NOT NULL COMMENT '操作后待履约基础单位数量', + CHANGE COLUMN `fulfilled_before` `fulfilled_base_before` DECIMAL(18,3) NOT NULL COMMENT '操作前已履约基础单位数量', + CHANGE COLUMN `fulfilled_after` `fulfilled_base_after` DECIMAL(18,3) NOT NULL COMMENT '操作后已履约基础单位数量'; + +-- 套餐配置继续使用 (package_config_id, sku_id, sku_unit_id) 唯一键:同 SKU 多单位合法,同 SKU 同单位重复由既有唯一键阻止 diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml index 7678d4d..aefc3a4 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml @@ -103,14 +103,14 @@ COUNT(DISTINCT o.`id`) AS `orderCount`, COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END), 0) AS `directQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END), 0) AS `packageQuantity`, - COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`, - COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`, - COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`, - COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`, - COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`, - COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`, - COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, - COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity` + COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedQuantity`, + COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`, + COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`base_quantity` ELSE 0 END), 0) AS `directBaseQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` ELSE 0 END), 0) AS `packageBaseQuantity`, + COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`, + COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`, + COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity` FROM `gao_sales_order_item` i INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` INNER JOIN `gao_product` p ON p.`id` = i.`product_id_snapshot` AND p.`store_id` = o.`store_id` diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml index 2f49939..2099756 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml @@ -127,9 +127,9 @@ COUNT(DISTINCT o.`id`) AS `orderCount`, COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`, - COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`, - COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, - COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity` + COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`, + COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`, + COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity` FROM `gao_sales_order_item` i INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` INNER JOIN `gao_product` p ON p.`id` = i.`product_id_snapshot` AND p.`store_id` = o.`store_id` diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml index a78f674..18f7130 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml @@ -86,14 +86,14 @@ COUNT(DISTINCT o.`id`) AS `orderCount`, COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END), 0) AS `directQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END), 0) AS `packageQuantity`, - COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`, - COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`, - COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`, - COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`, - COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`, - COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`, - COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, - COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`, + COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedQuantity`, + COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`, + COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`base_quantity` ELSE 0 END), 0) AS `directBaseQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` ELSE 0 END), 0) AS `packageBaseQuantity`, + COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`, + COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`, + COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN COALESCE(i.`line_amount`, 0) ELSE 0 END), 0) AS `directSaleAmount` FROM `gao_sales_order_item` i INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` @@ -124,11 +124,11 @@ i.`product_id_snapshot` AS `productId`, MAX(i.`product_name_snapshot`) AS `productName`, COUNT(DISTINCT o.`id`) AS `orderCount`, - COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`, - COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`, - COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`, - COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, - COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`base_quantity` ELSE 0 END), 0) AS `directBaseQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` ELSE 0 END), 0) AS `packageBaseQuantity`, + COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`, + COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`, + COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN COALESCE(i.`line_amount`, 0) ELSE 0 END), 0) AS `directSaleAmount` FROM `gao_sales_order_item` i INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml index 54cf638..3e8c505 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml @@ -20,16 +20,16 @@ WHERE `id` = #{id} AND `deleted_at` IS NULL FOR UPDATE - + UPDATE `gao_sales_order_item` - SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `reserved_quantity` + #{delta} >= 0 - AND `fulfilled_quantity` + `reserved_quantity` + #{delta} <= `quantity` + SET `reserved_base_quantity` = `reserved_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `reserved_base_quantity` + #{delta} >= 0 + AND `fulfilled_base_quantity` + `reserved_base_quantity` + #{delta} <= `base_quantity` - + UPDATE `gao_sales_order_item` - SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `fulfilled_quantity` + #{delta} >= 0 - AND `fulfilled_quantity` + #{delta} <= `quantity` + SET `fulfilled_base_quantity` = `fulfilled_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `fulfilled_base_quantity` + #{delta} >= 0 + AND `fulfilled_base_quantity` + #{delta} <= `base_quantity` From 2ea06fb5c0c2c41bd8c44a87ea5fd7e28dbc2746 Mon Sep 17 00:00:00 2001 From: Timi Date: Wed, 9 Sep 2026 01:49:52 +0800 Subject: [PATCH 5/5] v1.0.36 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a781910..0d4ae86 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.imyeyu.timiserverapi TimiServerAPI - 1.0.35 + 1.0.36 jar TimiServerAPI imyeyu.com API