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/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
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 extends SettingEnum> 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 extends Enum>> 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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;
}
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;
+ }
+}
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`;
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`