Compare commits

...
10 Commits
Author SHA1 Message Date
timi 3b19d9e930 Merge pull request 'v1.0.36' (#48) from dev into master
Reviewed-on: #48
2026-09-08 17:52:47 +00:00
Timi 2ea06fb5c0 v1.0.36
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-09 01:49:52 +08:00
Timi 00bb300755 support split fulfillment by base unit 2026-09-09 01:49:40 +08:00
Timi ea44dd5c0e update point rule groovy script 2026-09-09 01:08:45 +08:00
Timi 818207d6c9 fix FileController 2026-09-08 18:09:31 +08:00
Timi af76eb43b8 update SystemSetting 2026-09-08 16:52:32 +08:00
timi 5fd11e9caf Merge pull request 'v1.0.35' (#47) from dev into master
Reviewed-on: #47
2026-09-07 17:06:28 +00:00
Timi d920b8630d v1.0.35
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-08 01:05:45 +08:00
timi 0b98dcdfe9 Merge pull request 'v1.0.34' (#46) from dev into master
Reviewed-on: #46
2026-09-07 15:11:06 +00:00
Timi 1e62576d36 v1.0.34
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-07 23:10:27 +08:00
59 changed files with 828 additions and 738 deletions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$PROJECT_DIR$/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java" value="05742bdc-a11a-4147-9168-ef976a70258a" />
</component>
</project>
+1 -1
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId> <groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId> <artifactId>TimiServerAPI</artifactId>
<version>1.0.33</version> <version>1.0.36</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>TimiServerAPI</name> <name>TimiServerAPI</name>
<description>imyeyu.com API</description> <description>imyeyu.com API</description>
@@ -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";
}
@@ -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<Boolean> 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;
}
}
@@ -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();
}
@@ -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();
}
@@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.api.annotation.EnableSettingInterceptor;
import com.imyeyu.api.annotation.RequestRateLimitInterceptor; import com.imyeyu.api.annotation.RequestRateLimitInterceptor;
import com.imyeyu.api.annotation.RequireCorePermissionInterceptor; import com.imyeyu.api.annotation.RequireCorePermissionInterceptor;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor;
@@ -40,7 +39,6 @@ public class WebConfig implements WebMvcConfigurer {
private final ObjectMapper jackson; private final ObjectMapper jackson;
private final SystemAPIInterceptor systemAPIInterceptor; private final SystemAPIInterceptor systemAPIInterceptor;
private final JournalAPIInterceptor journalAPIInterceptor; private final JournalAPIInterceptor journalAPIInterceptor;
private final EnableSettingInterceptor enableSettingInterceptor;
private final RequestRateLimitInterceptor requestRateLimitInterceptor; private final RequestRateLimitInterceptor requestRateLimitInterceptor;
private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor; private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor;
private final RequireCorePermissionInterceptor requireCorePermissionInterceptor; private final RequireCorePermissionInterceptor requireCorePermissionInterceptor;
@@ -54,7 +52,6 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH); registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH);
registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH); registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH);
registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**"); registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**");
@@ -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 <T extends Enum<?>> SettingModule fromClass(Class<T> 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()));
}
}
@@ -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;
}
@@ -58,6 +58,14 @@ public class NotifyController {
@RequiredToken @RequiredToken
@PostMapping("/create") @PostMapping("/create")
public Notify create(@RequestBody Notify notify) { 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); service.create(notify);
return notify; return notify;
} }
@@ -53,6 +53,7 @@ public class SettingController {
*/ */
@JsonView(ResponseView.Admin.class) @JsonView(ResponseView.Admin.class)
@RequestRateLimit @RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_READ)
@PostMapping("/detail") @PostMapping("/detail")
public Setting detail(@RequestBody String id) { public Setting detail(@RequestBody String id) {
return service.get(id); return service.get(id);
@@ -66,6 +67,7 @@ public class SettingController {
*/ */
@AOPLog @AOPLog
@JsonView(ResponseView.Admin.class) @JsonView(ResponseView.Admin.class)
@RequireCorePermission(CorePermissionCode.SETTING_CREATE)
@RequestRateLimit @RequestRateLimit
@PostMapping("/create") @PostMapping("/create")
public Setting create(@RequestBody Setting setting) { public Setting create(@RequestBody Setting setting) {
@@ -80,6 +82,7 @@ public class SettingController {
*/ */
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_UPDATE)
@PostMapping("/update") @PostMapping("/update")
public void update(@RequestBody Setting setting) { public void update(@RequestBody Setting setting) {
service.update(setting); service.update(setting);
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.common.entity; package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -39,4 +40,7 @@ public class GroovyScript extends UUIDEntity {
/// 当前发布版本 ID /// 当前发布版本 ID
private String publishedVersionId; private String publishedVersionId;
@Transient
private GroovyScriptVersion latest;
} }
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.TimiServerAPI; import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.SettingModule;
import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.entity.UUIDEntity;
import com.imyeyu.spring.util.ResponseView; import com.imyeyu.spring.util.ResponseView;
@@ -62,6 +61,9 @@ public class Setting extends UUIDEntity {
/** 时间 */ /** 时间 */
TIME, TIME,
/// 字节大小
BYTE_SIZE,
/** 时长 */ /** 时长 */
DURATION, DURATION,
@@ -102,16 +104,6 @@ public class Setting extends UUIDEntity {
@Transient @Transient
private Enum<?> keyRaw; 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); 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,
}
}
} }
@@ -2,10 +2,14 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/// Groovy 脚本版本 Mapper /// Groovy 脚本版本 Mapper
/// ///
/// @author Codex /// @author Codex
/// @since 2026-09-04 /// @since 2026-09-04
public interface GroovyScriptVersionMapper extends BaseMapper<GroovyScriptVersion, String> { public interface GroovyScriptVersionMapper extends BaseMapper<GroovyScriptVersion, String> {
@Select("SELECT * FROM `groovy_script_version` WHERE `script_id` = #{scriptId} " + NOT_DELETE + " ORDER BY `version` DESC " + LIMIT_1)
GroovyScriptVersion selectLatest(String scriptId);
} }
@@ -1,8 +1,8 @@
package com.imyeyu.api.modules.common.service; 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.GroovyScript;
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
import java.util.List; import java.util.List;
@@ -16,6 +16,12 @@ import java.util.Map;
/// @since 2026-09-04 /// @since 2026-09-04
public interface GroovyScriptService extends BaseService<GroovyScript, String> { public interface GroovyScriptService extends BaseService<GroovyScript, String> {
/// 按业务归属查询脚本列表
///
/// @param bizType 业务归属
/// @return 脚本列表
List<GroovyScript> listByBizType(GroovyScript.BizType bizType);
/// 按业务归属和代码查询脚本 /// 按业务归属和代码查询脚本
/// ///
/// @param bizType 业务归属 /// @param bizType 业务归属
@@ -23,26 +29,14 @@ public interface GroovyScriptService extends BaseService<GroovyScript, String> {
/// @return 脚本,不存在时返回 null /// @return 脚本,不存在时返回 null
GroovyScript getByCode(GroovyScript.BizType bizType, String code); GroovyScript getByCode(GroovyScript.BizType bizType, String code);
/// 创建脚本元数据
///
/// 新建脚本默认停用,且没有已发布版本
///
/// @param script 脚本元数据
void create(GroovyScript script);
/// 更新脚本元数据
///
/// 业务归属和代码是脚本稳定标识,创建后不可变更
///
/// @param script 脚本元数据
void update(GroovyScript script);
/// 设置脚本启用状态 /// 设置脚本启用状态
/// ///
/// @param id 脚本 ID /// @param id 脚本 ID
/// @param enabled true 为允许执行已发布版本 /// @param enabled true 为允许执行已发布版本
void setEnabled(String id, boolean enabled); void setEnabled(String id, boolean enabled);
GroovyScriptVersion getLatestVersion(String scriptId);
/// 创建不可变的脚本版本 /// 创建不可变的脚本版本
/// ///
/// @param scriptId 脚本 ID /// @param scriptId 脚本 ID
@@ -67,11 +61,6 @@ public interface GroovyScriptService extends BaseService<GroovyScript, String> {
/// @param scriptId 脚本 ID /// @param scriptId 脚本 ID
void unpublish(String scriptId); void unpublish(String scriptId);
/// 删除脚本
///
/// @param id 脚本 ID
void delete(String id);
/// 执行已发布且启用的脚本 /// 执行已发布且启用的脚本
/// ///
/// @param bizType 业务归属 /// @param bizType 业务归属
@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.common.service; 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.bean.attachment.SettingSupport;
import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
@@ -18,8 +18,8 @@ public interface SettingService extends BaseService<Setting, String> {
Setting getByCode(String bizType, String bizId, String code); Setting getByCode(String bizType, String bizId, String code);
default Setting getSystem(Enum<?> bizType) { default Setting getSystem(SettingEnum setting) {
return getByCode(SettingModule.fromClass(bizType.getClass()).name(), TimiJava.ZERO_UUID, bizType.toString()); return getByCode(setting.getBizType(), TimiJava.ZERO_UUID, setting.name());
} }
List<Setting> listByBizId(String bizType, String bizId); List<Setting> listByBizId(String bizType, String bizId);
@@ -1,9 +1,9 @@
package com.imyeyu.api.modules.common.service.implement; package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; 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.GroovyScript;
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; 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.GroovyScriptMapper;
import com.imyeyu.api.modules.common.mapper.GroovyScriptVersionMapper; import com.imyeyu.api.modules.common.mapper.GroovyScriptVersionMapper;
import com.imyeyu.api.modules.common.service.GroovyScriptService; import com.imyeyu.api.modules.common.service.GroovyScriptService;
@@ -52,6 +52,14 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
return mapper; return mapper;
} }
@Override
public List<GroovyScript> listByBizType(GroovyScript.BizType bizType) {
TimiException.required(bizType, "未找到脚本业务归属");
GroovyScript example = new GroovyScript();
example.setBizType(bizType);
return mapper.selectAllByExample(example);
}
@Override @Override
public GroovyScript getByCode(GroovyScript.BizType bizType, String code) { public GroovyScript getByCode(GroovyScript.BizType bizType, String code) {
GroovyScript example = new GroovyScript(); GroovyScript example = new GroovyScript();
@@ -95,6 +103,11 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
invalidateAfterCommit(script.getId()); invalidateAfterCommit(script.getId());
} }
@Override
public GroovyScriptVersion getLatestVersion(String scriptId) {
return versionMapper.selectLatest(scriptId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public GroovyScriptVersion createVersion(String scriptId, String content) { public GroovyScriptVersion createVersion(String scriptId, String content) {
@@ -104,7 +117,7 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
GroovyScriptVersion version = new GroovyScriptVersion(); GroovyScriptVersion version = new GroovyScriptVersion();
version.setScriptId(scriptId); version.setScriptId(scriptId);
version.setVersion(nextVersion(scriptId)); version.setVersion(versionMapper.selectLatest(scriptId).getVersion() + 1);
version.setContent(content); version.setContent(content);
version.setChecksum(checksum(content)); version.setChecksum(checksum(content));
versionMapper.insert(version); versionMapper.insert(version);
@@ -218,13 +231,6 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
return version; return version;
} }
private int nextVersion(String scriptId) {
return listVersion(scriptId).stream()
.map(GroovyScriptVersion::getVersion)
.max(Integer::compareTo)
.orElse(0) + 1;
}
private String cacheKey(GroovyScriptVersion version) { private String cacheKey(GroovyScriptVersion version) {
return "%s:%s".formatted(version.getId(), version.getChecksum()); return "%s:%s".formatted(version.getId(), version.getChecksum());
} }
@@ -6,9 +6,9 @@ import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.mapper.NotifyMapper; import com.imyeyu.api.modules.common.mapper.NotifyMapper;
import com.imyeyu.api.modules.common.service.NotifyDetailService; import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.api.modules.common.service.NotifyService; import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
@@ -61,15 +61,13 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
try { try {
TimiException.required(notify, "未找到通知"); TimiException.required(notify, "未找到通知");
TimiException.required(notify.getDetailList(), "未找到通知详情列表"); TimiException.required(notify.getDetailList(), "未找到通知详情列表");
String sendFrom = userLoginService.isLogged() ? userLoginService.getRequireLoginUserId() : null;
notify.setCreatedBy(sendFrom); notify.setCreatedBy(TimiJava.defaultIfEmpty(notify.getCreatedBy(), TimiJava.ZERO_UUID));
super.create(notify); super.create(notify);
for (NotifyDetail detail : notify.getDetailList()) { for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId()); detail.setNotifyId(notify.getId());
if (TimiJava.isNotEmpty(sendFrom)) { detail.setSendFrom(TimiJava.defaultIfEmpty(detail.getSendFrom(), TimiJava.ZERO_UUID));
detail.setSendFrom(sendFrom);
}
detailService.create(detail); detailService.create(detail);
} }
logger.setLevel(Logger.Level.INFO); logger.setLevel(Logger.Level.INFO);
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.common.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.common.bean.TempFileSetting;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting; import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
@@ -52,11 +53,11 @@ public class TempFileServiceImplement implements TempFileService {
try { try {
logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl))); logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl)));
Setting ttlMin = settingService.getSystem(Setting.Module.TempFile.TTL_MIN); Setting ttlMin = settingService.getSystem(TempFileSetting.TTL_MIN);
Setting ttlMax = settingService.getSystem(Setting.Module.TempFile.TTL_MAX); Setting ttlMax = settingService.getSystem(TempFileSetting.TTL_MAX);
Setting ttlDefault = settingService.getSystem(Setting.Module.TempFile.TTL_DEFAULT); Setting ttlDefault = settingService.getSystem(TempFileSetting.TTL_DEFAULT);
Setting residueTime = settingService.getSystem(Setting.Module.TempFile.RESIDUE_TTL); Setting residueTime = settingService.getSystem(TempFileSetting.RESIDUE_TTL);
String limitStr = settingService.getSystem(Setting.Module.TempFile.LIMIT_SIZE_OF_IP).asString(); String limitStr = settingService.getSystem(TempFileSetting.LIMIT_SIZE_OF_IP).asString();
long limit = IOSize.parse(limitStr); long limit = IOSize.parse(limitStr);
@@ -1,8 +1,12 @@
package com.imyeyu.api.modules.gao.controller; package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService; import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPage; import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPageResult; import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPageResult;
@@ -15,6 +19,9 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/// GAO 客户消费事实接口 /// GAO 客户消费事实接口
/// ///
/// @author Codex /// @author Codex
@@ -24,8 +31,10 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/gao/customer/consumption-record") @RequestMapping("/gao/customer/consumption-record")
public class GaoCustomerConsumptionRecordController { public class GaoCustomerConsumptionRecordController {
private final GaoCustomerConsumptionRecordService service; private final AttachmentService attachmentService;
private final GaoCustomerService customerService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final GaoCustomerConsumptionRecordService service;
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@@ -34,6 +43,24 @@ public class GaoCustomerConsumptionRecordController {
public GaoCustomerConsumptionRecordPageResult list(@RequestBody GaoCustomerConsumptionRecordPage page) { public GaoCustomerConsumptionRecordPageResult list(@RequestBody GaoCustomerConsumptionRecordPage page) {
TimiException.required(page, "not found consumption record page"); TimiException.required(page, "not found consumption record page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return service.pageByQuery(page);
GaoCustomerConsumptionRecordPageResult result = service.pageByQuery(page);
List<String> customerIdList = result.getList()
.stream()
.map(GaoCustomerConsumptionRecord::getCustomerId)
.filter(customerId -> customerId != null && !customerId.isBlank())
.distinct()
.toList();
var customerMap = customerService.mapByIdList(customerIdList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList);
for (GaoCustomerConsumptionRecord record : result.getList()) {
var customer = customerMap.get(record.getCustomerId());
if (customer == null) {
continue;
}
customer.setAttachmentList(attachmentMap.getOrDefault(customer.getId(), List.of()));
record.setCustomer(customer);
}
return result;
} }
} }
@@ -12,6 +12,7 @@ import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
@@ -42,6 +43,7 @@ public class GaoFundRecordController {
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final UserService userService; private final UserService userService;
private final UserLoginService userLoginService;
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@@ -66,6 +68,33 @@ public class GaoFundRecordController {
return result; return result;
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/my-receive/list")
public PageResult<GaoFundRecord> listMyReceive(@RequestBody GaoFundRecordPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
page.setOperatorUserId(userLoginService.getRequireLoginUserId());
page.setOperationType(GaoFundRecord.OperationType.RECEIVE);
PageResult<GaoFundRecord> result = service.pageByQuery(page);
fillTransient(result.getList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/my-receive/detail")
public GaoFundRecord detailMyReceive(@RequestParam String id) {
GaoFundRecord result = service.detail(id);
TimiException.requiredTrue(result.getOperationType() == GaoFundRecord.OperationType.RECEIVE, "not found fund receive record");
TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(result.getOperatorUserId()), "只能读取自己提交的收款");
storeScopeService.checkStoreId(result.getStoreId());
fillTransient(List.of(result));
return result;
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REVOKE) @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE)
@@ -1,5 +1,7 @@
package com.imyeyu.api.modules.gao.controller; package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.common.entity.GroovyScript;
import com.imyeyu.api.modules.common.service.GroovyScriptService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoPointRule; import com.imyeyu.api.modules.gao.entity.GaoPointRule;
@@ -31,6 +33,7 @@ public class GaoPointRuleController {
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final GaoPointRuleService ruleService; private final GaoPointRuleService ruleService;
private final GroovyScriptService groovyScriptService;
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@@ -54,9 +57,22 @@ public class GaoPointRuleController {
GaoPointRule rule = ruleService.get(id); GaoPointRule rule = ruleService.get(id);
TimiException.required(rule, "not found point rule"); TimiException.required(rule, "not found point rule");
storeScopeService.checkStoreId(rule.getStoreId()); storeScopeService.checkStoreId(rule.getStoreId());
if (rule.getRuleMode() == GaoPointRule.RuleMode.GROOVY) {
GroovyScript script = groovyScriptService.get(rule.getGroovyScriptId());
script.setLatest(groovyScriptService.getLatestVersion(script.getId()));
rule.setGroovyScript(script);
}
return rule; return rule;
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ)
@PostMapping("/rule/script/list")
public List<GroovyScript> scriptList() {
return groovyScriptService.listByBizType(GroovyScript.BizType.GAO_POINT_RULE);
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE) @RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE)
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
@@ -112,7 +113,7 @@ public class GaoSalesOrderController {
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) @RequireGaoPermission(value = {GaoPermissionCode.FUND_RECEIVE_CREATE, GaoPermissionCode.FUND_RECORD_READ}, mode = PermissionMatchMode.ANY)
@PostMapping("/payment/cancel") @PostMapping("/payment/cancel")
public GaoFundRecord cancelPayment(@RequestBody GaoOrderPaymentActionRequest request) { public GaoFundRecord cancelPayment(@RequestBody GaoOrderPaymentActionRequest request) {
return paymentService.cancel(request); return paymentService.cancel(request);
@@ -29,7 +29,7 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// SKU ID /// SKU ID
private String skuId; private String skuId;
/// SKU 单位 ID /// 本次实际履约 SKU 单位 ID
private String skuUnitId; private String skuUnitId;
/// 产品图片附件 /// 产品图片附件
@@ -45,17 +45,17 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// 基础单位数量 /// 基础单位数量
private BigDecimal baseQuantity; 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; private String productNameSnapshot;
@@ -1,5 +1,7 @@
package com.imyeyu.api.modules.gao.entity; 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 com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -96,4 +98,7 @@ public class GaoPointRule extends UUIDEntity {
/// 备注 /// 备注
private String remark; private String remark;
@Transient
private GroovyScript groovyScript;
} }
@@ -71,11 +71,14 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 购买数量 /// 购买数量
private BigDecimal quantity; private BigDecimal quantity;
/// 待履约数量 /// 购买基础单位数量
private BigDecimal reservedQuantity; private BigDecimal baseQuantity;
/// 已履约数量 /// 待履约基础单位数量
private BigDecimal fulfilledQuantity; private BigDecimal reservedBaseQuantity;
/// 已履约基础单位数量
private BigDecimal fulfilledBaseQuantity;
/// 实际单价,单位分 /// 实际单价,单位分
private Long unitPrice; private Long unitPrice;
@@ -89,15 +92,15 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 备注 /// 备注
private String remark; private String remark;
/// 待核销数量 /// 待核销基础单位数量
@Transient @Transient
private BigDecimal pendingQuantity; private BigDecimal pendingBaseQuantity;
/// 当前可履约数量 /// 当前可履约基础单位数量
@Transient @Transient
private BigDecimal availableQuantity; private BigDecimal availableBaseQuantity;
/// 权益剩余数量 /// 权益剩余基础单位数量
@Transient @Transient
private BigDecimal remainingQuantity; private BigDecimal remainingBaseQuantity;
} }
@@ -22,9 +22,9 @@ public interface GaoSalesOrderItemMapper extends BaseMapper<GaoSalesOrderItem, S
/// 锁定订单明细 /// 锁定订单明细
GaoSalesOrderItem selectForUpdate(@Param("id") String id); GaoSalesOrderItem selectForUpdate(@Param("id") String id);
/// 原子调整待履约数量 /// 原子调整待履约基础单位数量
int updateReservedQuantity(@Param("id") String id, @Param("delta") BigDecimal delta); int updateReservedBaseQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
/// 原子调整已履约数量 /// 原子调整已履约基础单位数量
int updateFulfilledQuantity(@Param("id") String id, @Param("delta") BigDecimal delta); int updateFulfilledBaseQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
} }
@@ -22,7 +22,7 @@ public interface GaoOrderPaymentService {
/// 拒绝收款 /// 拒绝收款
GaoFundRecord reject(GaoOrderPaymentActionRequest request); GaoFundRecord reject(GaoOrderPaymentActionRequest request);
/// 撤销自己提交的收款 /// 撤销待核销收款
GaoFundRecord cancel(GaoOrderPaymentActionRequest request); GaoFundRecord cancel(GaoOrderPaymentActionRequest request);
/// 登记退款 /// 登记退款
@@ -58,11 +58,11 @@ public interface GaoSalesOrderService extends BaseService<GaoSalesOrder, String>
/// 检查履约权限 /// 检查履约权限
void checkFulfillmentPermission(GaoSalesOrder order); 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); GaoSalesOrderItem getItemForUpdate(String itemId);
@@ -1,11 +1,12 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord; import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerConsumptionRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoCustomerConsumptionRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService; import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPage; import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPageResult; import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPageResult;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
@@ -28,6 +29,7 @@ public class GaoCustomerConsumptionRecordServiceImplement extends AbstractEntity
private final GaoCustomerConsumptionRecordMapper mapper; private final GaoCustomerConsumptionRecordMapper mapper;
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final AttachmentService attachmentService;
@Override @Override
protected BaseMapper<GaoCustomerConsumptionRecord, String> mapper() { protected BaseMapper<GaoCustomerConsumptionRecord, String> mapper() {
@@ -40,12 +42,7 @@ public class GaoCustomerConsumptionRecordServiceImplement extends AbstractEntity
GaoCustomerConsumptionRecordPageResult result = new GaoCustomerConsumptionRecordPageResult(); GaoCustomerConsumptionRecordPageResult result = new GaoCustomerConsumptionRecordPageResult();
result.setTotal(mapper.countByQuery(page)); result.setTotal(mapper.countByQuery(page));
result.setAmountTotal(mapper.sumAmountByPage(page)); result.setAmountTotal(mapper.sumAmountByPage(page));
List<GaoCustomerConsumptionRecord> list = mapper.selectByQuery(page); result.setList(mapper.selectByQuery(page));
result.setList(list);
var customerMap = customerService.mapByIdList(list.stream().map(GaoCustomerConsumptionRecord::getCustomerId).toList());
for (GaoCustomerConsumptionRecord record : list) {
record.setCustomer(customerMap.get(record.getCustomerId()));
}
return result; return result;
} }
@@ -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.GaoCustomerPackagePurchaseRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem;
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
@@ -38,6 +39,10 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; 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.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -53,6 +58,8 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoCustomerPackageServiceImplement extends AbstractEntityService<GaoCustomerPackage, String> implements GaoCustomerPackageService { public class GaoCustomerPackageServiceImplement extends AbstractEntityService<GaoCustomerPackage, String> implements GaoCustomerPackageService {
private static final BigDecimal ZERO = BigDecimal.ZERO;
private final GaoCustomerPackageMapper mapper; private final GaoCustomerPackageMapper mapper;
private final GaoPackageConfigService packageConfigService; private final GaoPackageConfigService packageConfigService;
private final GaoProductSkuService skuService; private final GaoProductSkuService skuService;
@@ -143,9 +150,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
if (request.getItemList() == null) { if (request.getItemList() == null) {
request.setItemList(defaultPurchaseItems(configItemList)); request.setItemList(defaultPurchaseItems(configItemList));
} }
List<GaoCustomerPackagePurchaseItem> itemList = new java.util.ArrayList<>(request.getItemList()); List<GaoCustomerPackagePurchaseItem> requestItemList = new ArrayList<>(request.getItemList());
verifyPurchaseItems(itemList); verifyPurchaseItems(requestItemList);
itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort)); boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO,
com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
List<GaoCustomerPackagePurchaseItem> itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList, canOverridePriceAndQuantity);
itemList.sort(Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort).thenComparing(GaoCustomerPackagePurchaseItem::getSkuId)
.thenComparing(GaoCustomerPackagePurchaseItem::getSkuUnitId));
request.setItemList(itemList); request.setItemList(itemList);
GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo()); GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo());
@@ -154,19 +165,14 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
verifyPurchaseIdempotency(existingOrder, existingPackage, request); verifyPurchaseIdempotency(existingOrder, existingPackage, request);
return existingPackage; return existingPackage;
} }
verifyConfigSellableItems(config.getStoreId(), configItemList);
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now()); Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
Long now = Time.now(); Long now = Time.now();
TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买"); TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买");
TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效"); TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效");
TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期"); TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期");
GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId()); GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId());
verifySameStructureAsConfig(itemList, configItemList);
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO, com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
if (!canOverridePriceAndQuantity) { if (!canOverridePriceAndQuantity) {
TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价"); TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价");
verifySameQuantityAsConfig(itemList, configItemList);
} }
List<GaoSalesOrderItem> orderItemList = itemList.stream().map(this::toOrderItem).toList(); List<GaoSalesOrderItem> orderItemList = itemList.stream().map(this::toOrderItem).toList();
@@ -267,8 +273,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
item.setUnitSnapshot(source.getUnitSnapshot()); item.setUnitSnapshot(source.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
item.setQuantity(source.getTotalQuantity()); item.setQuantity(source.getTotalQuantity());
item.setReservedQuantity(BigDecimal.ZERO); item.setBaseQuantity(toBaseQuantity(source.getTotalQuantity(), source.getBaseQuantityPerUnitSnapshot(), "套餐权益基础数量最多保留三位小数"));
item.setFulfilledQuantity(BigDecimal.ZERO); item.setReservedBaseQuantity(ZERO);
item.setFulfilledBaseQuantity(ZERO);
item.setUnitPrice(null); item.setUnitPrice(null);
item.setLineAmount(null); item.setLineAmount(null);
item.setSort(source.getSort()); item.setSort(source.getSort());
@@ -277,7 +284,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
} }
private void verifyPurchaseItems(List<GaoCustomerPackagePurchaseItem> itemList) { private void verifyPurchaseItems(List<GaoCustomerPackagePurchaseItem> itemList) {
TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品");
Set<String> keySet = new HashSet<>(); Set<String> keySet = new HashSet<>();
for (int i = 0; i < itemList.size(); i++) { for (int i = 0; i < itemList.size(); i++) {
GaoCustomerPackagePurchaseItem item = itemList.get(i); GaoCustomerPackagePurchaseItem item = itemList.get(i);
@@ -285,7 +291,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
TimiException.required(item.getSkuId(), "购买 SKU 不能为空"); TimiException.required(item.getSkuId(), "购买 SKU 不能为空");
TimiException.required(item.getSkuUnitId(), "购买 SKU 单位不能为空"); TimiException.required(item.getSkuUnitId(), "购买 SKU 单位不能为空");
TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空"); TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空");
TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0"); TimiException.requiredTrue(0 <= item.getTotalQuantity().signum(), "购买产品数量不能为负数");
TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数"); TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
String key = item.getSkuId() + "|" + item.getSkuUnitId(); String key = item.getSkuId() + "|" + item.getSkuUnitId();
TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复"); TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
@@ -293,38 +299,69 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
} }
} }
private void verifyConfigSellableItems(String storeId, List<GaoPackageConfigItem> itemList) { private List<GaoCustomerPackagePurchaseItem> resolvePurchaseItems(String storeId, List<GaoCustomerPackagePurchaseItem> requestItemList,
for (GaoPackageConfigItem item : itemList) { List<GaoPackageConfigItem> configItemList, boolean canOverridePriceAndQuantity) {
TimiException.required(item.getSkuId(), "套餐配置缺少 SKU"); Map<String, List<GaoPackageConfigItem>> configItemMap = configItemList.stream()
TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位"); .collect(Collectors.groupingBy(GaoPackageConfigItem::getSkuId));
// 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态 Map<String, BigDecimal> configBaseQuantityMap = new HashMap<>();
skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId()); for (GaoPackageConfigItem configItem : configItemList) {
configBaseQuantityMap.merge(configItem.getSkuId(),
toBaseQuantity(configItem.getDefaultQuantity(), configItem.getBaseQuantityPerUnitSnapshot(), "套餐配置基础数量最多保留三位小数"), BigDecimal::add);
} }
Map<String, BigDecimal> requestBaseQuantityMap = new HashMap<>();
List<GaoCustomerPackagePurchaseItem> result = new ArrayList<>();
for (GaoCustomerPackagePurchaseItem requestItem : requestItemList) {
List<GaoPackageConfigItem> 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<String, BigDecimal> entry : configBaseQuantityMap.entrySet()) {
TimiException.requiredTrue(entry.getValue().compareTo(requestBaseQuantityMap.getOrDefault(entry.getKey(), ZERO)) == 0,
"普通员工不能修改套餐权益配额");
}
}
return result;
} }
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) { private GaoPackageConfigItem resolveItemTemplate(List<GaoPackageConfigItem> configItemList, String skuUnitId) {
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致"); return configItemList.stream().filter(item -> skuUnitId.equals(item.getSkuUnitId())).findFirst()
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); .orElseGet(() -> configItemList.stream().min(Comparator.comparing(GaoPackageConfigItem::getSort)).orElseThrow());
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 void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) { private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); BigDecimal result = quantity.multiply(baseQuantityPerUnit);
for (GaoPackageConfigItem configItem : configItemList) { TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId()); try {
TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额"); return result.setScale(3, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, message, e);
} }
} }
@@ -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.GaoOrderFulfillmentPage;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; 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.gao.vo.GaoStockChangeItem;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
@@ -46,6 +47,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@@ -194,12 +196,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId()); GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment"); TimiException.required(snapshot, "not found fulfillment");
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId()); GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false); List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
storeScopeService.checkStoreId(fulfillment.getStoreId()); storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态"); TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存"); TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存");
salesOrderService.checkFulfillmentPermission(order);
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList); List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
@@ -228,12 +229,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId()); GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment"); TimiException.required(snapshot, "not found fulfillment");
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId()); GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false); List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
storeScopeService.checkStoreId(fulfillment.getStoreId()); storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态"); TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约"); TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约");
salesOrderService.checkFulfillmentPermission(order);
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
assertReserved(sourceList); assertReserved(sourceList);
@@ -283,7 +283,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId()); GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment"); TimiException.required(snapshot, "not found fulfillment");
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId()); GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false); List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
storeScopeService.checkStoreId(fulfillment.getStoreId()); storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK
@@ -326,7 +326,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
} }
GaoSalesOrder order = salesOrderService.getForUpdate(sourceSnapshot.getSalesOrderId()); GaoSalesOrder order = salesOrderService.getForUpdate(sourceSnapshot.getSalesOrderId());
List<GaoOrderFulfillmentItem> sourceItemList = itemMapper.selectByFulfillmentId(sourceSnapshot.getId()); List<GaoOrderFulfillmentItem> sourceItemList = itemMapper.selectByFulfillmentId(sourceSnapshot.getId());
List<SourceEntry> sourceList = loadSources(order, copyRequestItems(sourceItemList), false); List<SourceEntry> sourceList = loadPersistedSources(order, sourceItemList);
GaoOrderFulfillment source = lockFulfillment(sourceSnapshot.getId(), order); GaoOrderFulfillment source = lockFulfillment(sourceSnapshot.getId(), order);
storeScopeService.checkStoreId(source.getStoreId()); storeScopeService.checkStoreId(source.getStoreId());
TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正"); TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正");
@@ -356,10 +356,10 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
TimiException.required(sourceItem, "履约明细来源订单项不存在"); TimiException.required(sourceItem, "履约明细来源订单项不存在");
GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem); GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem);
reverseItem.setFulfillmentId(reverse.getId()); reverseItem.setFulfillmentId(reverse.getId());
reverseItem.setReservedBefore(sourceEntry.reservedBefore()); reverseItem.setReservedBaseBefore(sourceEntry.reservedBaseBefore());
reverseItem.setReservedAfter(sourceEntry.reservedBefore()); reverseItem.setReservedBaseAfter(sourceEntry.reservedBaseBefore());
reverseItem.setFulfilledBefore(sourceEntry.fulfilledBefore()); reverseItem.setFulfilledBaseBefore(sourceEntry.fulfilledBaseBefore());
reverseItem.setFulfilledAfter(sourceEntry.fulfilledBefore().subtract(sourceEntry.quantity())); reverseItem.setFulfilledBaseAfter(sourceEntry.fulfilledBaseBefore().subtract(sourceEntry.baseQuantity()));
itemMapper.insert(reverseItem); itemMapper.insert(reverseItem);
} }
if (!stockItems.isEmpty()) { if (!stockItems.isEmpty()) {
@@ -397,28 +397,28 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private void insertSourceReservations(GaoOrderFulfillment fulfillment, List<SourceEntry> sourceList) { private void insertSourceReservations(GaoOrderFulfillment fulfillment, List<SourceEntry> sourceList) {
for (SourceEntry source : 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()); GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId());
itemMapper.insert(item); itemMapper.insert(item);
} }
} }
private void decreaseReserved(SourceEntry source) { 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) { 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) { private void decreaseFulfilled(SourceEntry source) {
TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正"); TimiException.requiredTrue(0 <= source.fulfilledBaseBefore().compareTo(source.baseQuantity()), "已履约数量不足以冲正");
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败"); TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细冲正失败");
} }
private void assertReserved(List<SourceEntry> sourceList) { private void assertReserved(List<SourceEntry> sourceList) {
for (SourceEntry source : 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<G
TimiException.required(requestItem, "履约明细不能为空"); TimiException.required(requestItem, "履约明细不能为空");
String sourceId = requiredText(requestItem.getSourceOrderItemId(), "履约明细来源订单项不能为空"); String sourceId = requiredText(requestItem.getSourceOrderItemId(), "履约明细来源订单项不能为空");
requestItem.setSourceOrderItemId(sourceId); requestItem.setSourceOrderItemId(sourceId);
requestItem.setSkuUnitId(requiredText(requestItem.getSkuUnitId(), "履约 SKU 单位不能为空"));
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细"); TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
TimiException.required(requestItem.getQuantity(), "履约数量不能为空"); TimiException.required(requestItem.getQuantity(), "履约数量不能为空");
TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0"); TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0");
@@ -444,40 +445,73 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT; ? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配"); TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
result.add(toSource(order, source, requestItem.getQuantity(), checkAvailable)); GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(order.getStoreId(), source.getSkuId(), requestItem.getSkuUnitId());
result.add(toSource(order, source, selection.getProduct(), selection.getSku(), selection.getUnit(), requestItem.getQuantity(), checkAvailable));
} }
return result; return result;
} }
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) { private List<SourceEntry> loadPersistedSources(GaoSalesOrder order, List<GaoOrderFulfillmentItem> itemList) {
GaoProductSku sku = skuService.get(source.getSkuId()); TimiException.requiredTrue(itemList != null && !itemList.isEmpty(), "履约单没有明细");
TimiException.required(sku, "履约来源 SKU 不存在"); List<GaoOrderFulfillmentItem> sortedItemList = new ArrayList<>(itemList);
GaoProduct product = productService.get(sku.getProductId()); sortedItemList.sort(Comparator.comparing(GaoOrderFulfillmentItem::getSourceOrderItemId));
TimiException.required(product, "履约来源产品不存在"); Set<String> sourceIdSet = new HashSet<>();
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店"); List<SourceEntry> result = new ArrayList<>();
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot()); for (GaoOrderFulfillmentItem item : sortedItemList) {
GaoProductSkuUnit unit = snapshotUnit(source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit); TimiException.required(item, "履约明细不能为空");
JsonNode skuSpecSnapshot = source.getSkuSpecSnapshot(); String sourceId = requiredText(item.getSourceOrderItemId(), "履约明细来源订单项不能为空");
return buildSource(order, source.getId(), source.getSkuId(), source.getSkuUnitId(), source.getProductIdSnapshot(), source.getProductNameSnapshot(), source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), skuSpecSnapshot, TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(), GaoSalesOrderItem source = salesOrderService.getItemForUpdate(sourceId);
product, sku, unit, checkAvailable); 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, private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, GaoProduct product, GaoProductSku sku,
String productName, String skuCode, String skuName, String specSummary, JsonNode skuSpecSnapshot, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity, GaoProductSkuUnit unit, BigDecimal quantity, boolean checkAvailable) {
BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit, TimiException.requiredTrue(source.getSkuId().equals(sku.getId()), "履约单位不属于来源 SKU");
boolean checkAvailable) { TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
quantity = requiredPositiveQuantity(quantity, "履约数量");
if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
} }
BigDecimal reservedBefore = decimal(reservedQuantity); BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(unit.getBaseQuantityPerUnit());
BigDecimal fulfilledBefore = decimal(fulfilledQuantity); 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) { if (checkAvailable) {
BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore); BigDecimal availableBaseQuantity = totalBaseQuantity.subtract(reservedBaseBefore).subtract(fulfilledBaseBefore);
TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足"); TimiException.requiredTrue(0 <= availableBaseQuantity.compareTo(baseQuantity), "来源产品可履约数量不足");
} }
return new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, skuSpecSnapshot, unitName, baseQuantityPerUnit, return new SourceEntry(source.getId(), source.getSkuId(), unitId, source.getProductIdSnapshot(), source.getProductNameSnapshot(),
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit); 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<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) { private List<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) {
@@ -489,7 +523,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoProductSkuUnit baseUnit = skuService.listUnitBySkuId(source.sku().getId()).stream() GaoProductSkuUnit baseUnit = skuService.listUnitBySkuId(source.sku().getId()).stream()
.filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null); .filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null);
TimiException.required(baseUnit, "实物产品缺少基础库存单位"); TimiException.required(baseUnit, "实物产品缺少基础库存单位");
BigDecimal baseQuantity = source.quantity().multiply(source.unit().getBaseQuantityPerUnit()); BigDecimal baseQuantity = source.baseQuantity();
GaoStockChangeItem item = itemMap.get(source.sku().getId()); GaoStockChangeItem item = itemMap.get(source.sku().getId());
if (item == null) { if (item == null) {
item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity); item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity);
@@ -528,21 +562,6 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
} }
} }
private List<GaoOrderFulfillmentItemRequest> itemRequests(String fulfillmentId) {
return copyRequestItems(itemMapper.selectByFulfillmentId(fulfillmentId));
}
private List<GaoOrderFulfillmentItemRequest> copyRequestItems(List<GaoOrderFulfillmentItem> itemList) {
List<GaoOrderFulfillmentItemRequest> result = new ArrayList<>();
for (GaoOrderFulfillmentItem item : TimiJava.safeIterable(itemList)) {
GaoOrderFulfillmentItemRequest request = new GaoOrderFulfillmentItemRequest();
request.setSourceOrderItemId(item.getSourceOrderItemId());
request.setQuantity(item.getQuantity());
result.add(request);
}
return result;
}
private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) { private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setSkuId(source.getSkuId()); result.setSkuId(source.getSkuId());
@@ -561,20 +580,36 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return result; return result;
} }
private GaoProductSkuUnit snapshotUnit(String id, String name, BigDecimal baseQuantityPerUnit) {
GaoProductSkuUnit unit = new GaoProductSkuUnit();
unit.setId(id);
unit.setName(name);
unit.setBaseQuantityPerUnit(baseQuantityPerUnit);
return unit;
}
private BigDecimal requiredBaseQuantityPerUnit(BigDecimal value) { private BigDecimal requiredBaseQuantityPerUnit(BigDecimal value) {
TimiException.required(value, "履约来源缺少基础单位数量系数快照"); TimiException.required(value, "履约来源缺少基础单位数量系数快照");
TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效"); TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效");
return value; return value;
} }
private BigDecimal requiredPositiveQuantity(BigDecimal value, String fieldName) {
TimiException.required(value, fieldName + "不能为空");
TimiException.requiredTrue(0 < value.signum(), fieldName + "必须大于 0");
TimiException.requiredTrue(3 >= 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) { private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) {
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId()) TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
@@ -582,12 +617,14 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId()); List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId());
List<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList(); List<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList();
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致"); TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
Map<String, BigDecimal> requestMap = new HashMap<>(); Map<String, GaoOrderFulfillmentItemRequest> requestMap = new HashMap<>();
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) { for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) {
requestMap.put(item.getSourceOrderItemId(), item.getQuantity()); requestMap.put(item.getSourceOrderItemId(), item);
} }
for (GaoOrderFulfillmentItem item : existingItems) { 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<G
private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot, private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot,
String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, JsonNode skuSpecSnapshot, String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, JsonNode skuSpecSnapshot,
String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity, String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity,
BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, GaoProductSku sku, BigDecimal baseQuantity, BigDecimal totalBaseQuantity, BigDecimal reservedBaseBefore, BigDecimal fulfilledBaseBefore,
GaoProductSkuUnit unit) { GaoProduct product, GaoProductSku sku) {
private String sourceId() { private String sourceId() {
return sourceOrderItemId; return sourceOrderItemId;
@@ -686,11 +723,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
result.setProductIdSnapshot(productIdSnapshot); result.setProductIdSnapshot(productIdSnapshot);
result.setSourceOrderItemId(sourceOrderItemId); result.setSourceOrderItemId(sourceOrderItemId);
result.setQuantity(quantity); result.setQuantity(quantity);
result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3)); result.setBaseQuantity(baseQuantity);
result.setReservedBefore(reservedBefore); result.setReservedBaseBefore(reservedBaseBefore);
result.setReservedAfter(reservedBefore.add(quantity)); result.setReservedBaseAfter(reservedBaseBefore.add(baseQuantity));
result.setFulfilledBefore(fulfilledBefore); result.setFulfilledBaseBefore(fulfilledBaseBefore);
result.setFulfilledAfter(fulfilledBefore); result.setFulfilledBaseAfter(fulfilledBaseBefore);
result.setProductNameSnapshot(productNameSnapshot); result.setProductNameSnapshot(productNameSnapshot);
result.setSkuCodeSnapshot(skuCodeSnapshot); result.setSkuCodeSnapshot(skuCodeSnapshot);
result.setSkuNameSnapshot(skuNameSnapshot); result.setSkuNameSnapshot(skuNameSnapshot);
@@ -1,10 +1,12 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService; import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
@@ -17,6 +19,7 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
@@ -40,6 +43,7 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
private final GaoPointTriggerService pointTriggerService; private final GaoPointTriggerService pointTriggerService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final PermissionChecker permissionChecker;
private final LoggerService loggerService; private final LoggerService loggerService;
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@@ -110,7 +114,8 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public GaoFundRecord cancel(GaoOrderPaymentActionRequest request) { public GaoFundRecord cancel(GaoOrderPaymentActionRequest request) {
return releasePending(request, GaoFundRecord.Status.CANCELLED, true); boolean onlyOperator = !permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.FUND_RECORD_READ.getValue());
return releasePending(request, GaoFundRecord.Status.CANCELLED, onlyOperator);
} }
private GaoFundRecord releasePending(GaoOrderPaymentActionRequest request, GaoFundRecord.Status targetStatus, boolean onlyOperator) { private GaoFundRecord releasePending(GaoOrderPaymentActionRequest request, GaoFundRecord.Status targetStatus, boolean onlyOperator) {
@@ -68,7 +68,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
GaoPointScriptContext context = eventContext(rule, record); GaoPointScriptContext context = eventContext(rule, record);
GroovyScriptExecutionResult<GaoPointDecision> execution = evaluate(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), GroovyScriptExecutionResult<GaoPointDecision> execution = evaluate(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(),
GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId()); GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId());
if (execution == null) { if (execution.result() == null) {
continue; continue;
} }
pointDeltaTotal += applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), null, pointDeltaTotal += applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), null,
@@ -73,6 +73,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
private static final BigDecimal ZERO = BigDecimal.ZERO; private static final BigDecimal ZERO = BigDecimal.ZERO;
// 过渡兼容:旧事件登记系统已收款,但新订单可能仍为零有效收款,暂时允许店长发起履约;过渡结束改为 false 并删除此分支
private static final boolean ALLOW_STORE_MANAGER_ZERO_PAYMENT_FULFILLMENT = true;
private final GaoSalesOrderMapper mapper; private final GaoSalesOrderMapper mapper;
private final GaoSalesOrderItemMapper itemMapper; private final GaoSalesOrderItemMapper itemMapper;
private final GaoFundRecordService fundRecordService; private final GaoFundRecordService fundRecordService;
@@ -280,18 +283,21 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) { if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) {
return; return;
} }
TimiException.requiredTrue(0 < safe(order.getPaidAmount()), "订单未有已核销收款,不能履约"); if (ALLOW_STORE_MANAGER_ZERO_PAYMENT_FULFILLMENT) {
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name()); roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
return;
}
TimiException.requiredTrue(false, "订单未全额收款,不能发起履约");
} }
@Override @Override
public int updateItemReservedQuantity(String itemId, BigDecimal delta) { public int updateItemReservedBaseQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateReservedQuantity(itemId, delta); return itemMapper.updateReservedBaseQuantity(itemId, delta);
} }
@Override @Override
public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) { public int updateItemFulfilledBaseQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateFulfilledQuantity(itemId, delta); return itemMapper.updateFulfilledBaseQuantity(itemId, delta);
} }
@Override @Override
@@ -309,7 +315,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId()); List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId());
boolean fulfilled = !itemList.isEmpty(); boolean fulfilled = !itemList.isEmpty();
for (GaoSalesOrderItem item : itemList) { for (GaoSalesOrderItem item : itemList) {
if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) { if (decimal(item.getFulfilledBaseQuantity()).compareTo(decimal(item.getBaseQuantity())) < 0) {
fulfilled = false; fulfilled = false;
break; break;
} }
@@ -383,8 +389,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
item.setUnitSnapshot(unit.getName()); item.setUnitSnapshot(unit.getName());
item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit()); item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit());
item.setQuantity(quantity); item.setQuantity(quantity);
item.setReservedQuantity(ZERO); item.setBaseQuantity(toBaseQuantity(quantity, unit.getBaseQuantityPerUnit(), "订单项基础单位数量最多保留三位小数"));
item.setFulfilledQuantity(ZERO); item.setReservedBaseQuantity(ZERO);
item.setFulfilledBaseQuantity(ZERO);
item.setUnitPrice(unitPrice); item.setUnitPrice(unitPrice);
item.setLineAmount(lineAmount); item.setLineAmount(lineAmount);
item.setSort(TimiJava.defaultIfNull(source.getSort(), i)); item.setSort(TimiJava.defaultIfNull(source.getSort(), i));
@@ -454,11 +461,14 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数"); TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数");
TimiException.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空"); TimiException.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空");
TimiException.requiredTrue(0 < item.getBaseQuantityPerUnitSnapshot().signum(), "订单项单位换算快照必须大于 0"); 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) { if (expectedItemType == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT) {
TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价"); TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价");
@@ -544,12 +554,12 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
/// ///
/// @param item 订单项 /// @param item 订单项
private void fillItemBalance(GaoSalesOrderItem item) { private void fillItemBalance(GaoSalesOrderItem item) {
BigDecimal quantity = decimal(item.getQuantity()); BigDecimal baseQuantity = decimal(item.getBaseQuantity());
BigDecimal reserved = decimal(item.getReservedQuantity()); BigDecimal reservedBaseQuantity = decimal(item.getReservedBaseQuantity());
BigDecimal fulfilled = decimal(item.getFulfilledQuantity()); BigDecimal fulfilledBaseQuantity = decimal(item.getFulfilledBaseQuantity());
item.setPendingQuantity(reserved); item.setPendingBaseQuantity(reservedBaseQuantity);
item.setAvailableQuantity(quantity.subtract(reserved).subtract(fulfilled)); item.setAvailableBaseQuantity(baseQuantity.subtract(reservedBaseQuantity).subtract(fulfilledBaseQuantity));
item.setRemainingQuantity(quantity.subtract(fulfilled)); item.setRemainingBaseQuantity(baseQuantity.subtract(fulfilledBaseQuantity));
} }
private void fillItemAttachments(List<GaoSalesOrderItem> itemList) { private void fillItemAttachments(List<GaoSalesOrderItem> itemList) {
@@ -606,6 +616,24 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
return quantity; return quantity;
} }
/// 转换基础单位数量
///
/// @param quantity 当前单位数量
/// @param baseQuantityPerUnit 当前单位折算基础单位系数
/// @param message 精度错误信息
/// @return 三位小数的基础单位数量
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
TimiException.required(quantity, "数量不能为空");
TimiException.required(baseQuantityPerUnit, "基础单位数量系数不能为空");
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 long toCentAmount(BigDecimal quantity, long unitPrice) { private long toCentAmount(BigDecimal quantity, long unitPrice) {
try { try {
return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact(); return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact();
@@ -39,4 +39,7 @@ public class GaoFundRecordPage extends BasePage {
/// 操作结束时间 /// 操作结束时间
private Long endAt; private Long endAt;
/// 操作用户 ID,仅限服务端设置的数据范围条件
private String operatorUserId;
} }
@@ -14,6 +14,9 @@ public class GaoOrderFulfillmentItemRequest {
/// 来源销售订单明细 ID /// 来源销售订单明细 ID
private String sourceOrderItemId; private String sourceOrderItemId;
/// 本次实际履约 SKU 单位 ID
private String skuUnitId;
/// 本次履约数量 /// 本次履约数量
private BigDecimal quantity; private BigDecimal quantity;
} }
@@ -25,14 +25,15 @@ public class ToolController {
@GetMapping("/memo") @GetMapping("/memo")
public String getMemo() { public String getMemo() {
return settingService.getSystem(Setting.Module.Journal.MEMO).asString(); // return settingService.getSystem(Setting.Module.Journal.MEMO).asString();
return null;
} }
@RequiredUploadPermission @RequiredUploadPermission
@PostMapping("/memo/update") @PostMapping("/memo/update")
public void updateMemo(@RequestBodyValue String data) { public void updateMemo(@RequestBodyValue String data) {
Setting setting = settingService.getSystem(Setting.Module.Journal.MEMO); // Setting setting = settingService.getSystem(Setting.Module.Journal.MEMO);
setting.setValue(data); // setting.setValue(data);
settingService.update(setting); // settingService.update(setting);
} }
} }
@@ -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;
}
@@ -1,8 +1,8 @@
package com.imyeyu.api.modules.music.runner; package com.imyeyu.api.modules.music.runner;
import com.imyeyu.api.config.CORSConfig; 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.common.service.SettingService;
import com.imyeyu.api.modules.music.bean.MusicSetting;
import com.imyeyu.api.modules.music.handler.ControllerMessageHandler; import com.imyeyu.api.modules.music.handler.ControllerMessageHandler;
import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel; import io.netty.channel.Channel;
@@ -62,7 +62,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
} }
public void run(ApplicationArguments args) { public void run(ApplicationArguments args) {
if (settingService.getSystem(Setting.Module.Music.ENABLE).isTrue()) { if (settingService.getSystem(MusicSetting.ENABLE).isTrue()) {
return; return;
} }
@@ -75,7 +75,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
ServerBootstrap bootstrap = new ServerBootstrap(); ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker); bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class); 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<SocketChannel>() { bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override @Override
@@ -89,7 +89,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
@Override @Override
public void channelRead(ChannelHandlerContext ctx, Object obj) throws Exception { public void channelRead(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof FullHttpRequest req) { 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 // 访问的路径不是 Web Socket 的端点地址,响应 404
ctx.channel().writeAndFlush(NFP).addListener(ChannelFutureListener.CLOSE); ctx.channel().writeAndFlush(NFP).addListener(ChannelFutureListener.CLOSE);
return; return;
@@ -111,10 +111,10 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
}); });
pipeline.addLast(new WebSocketServerCompressionHandler()); pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler( pipeline.addLast(new WebSocketServerProtocolHandler(
settingService.getSystem(Setting.Module.Music.CONTROLLER_URI).asString(), settingService.getSystem(MusicSetting.CONTROLLER_URI).asString(),
null, null,
true, 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(); 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) { } catch (Exception e) {
log.error("TimiMusicRC controller service startup error", e); log.error("TimiMusicRC controller service startup error", e);
} }
@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.music.runner; 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.common.service.SettingService;
import com.imyeyu.api.modules.music.bean.MusicSetting;
import com.imyeyu.api.modules.music.handler.PlayerMessageHandler; import com.imyeyu.api.modules.music.handler.PlayerMessageHandler;
import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel; import io.netty.channel.Channel;
@@ -49,7 +49,7 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
} }
public void run(ApplicationArguments args) { public void run(ApplicationArguments args) {
if (settingService.getSystem(Setting.Module.Music.ENABLE).isTrue()) { if (settingService.getSystem(MusicSetting.ENABLE).isTrue()) {
return; return;
} }
ServerBootstrap bootstrap = new ServerBootstrap(); ServerBootstrap bootstrap = new ServerBootstrap();
@@ -58,13 +58,13 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
try { try {
bootstrap.group(boss, worker); bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class); 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<>() { bootstrap.childHandler(new ChannelInitializer<>() {
@Override @Override
protected void initChannel(Channel ch) { protected void initChannel(Channel ch) {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder( ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
settingService.getSystem(Setting.Module.Music.MAX_FRAME_LENGTH).asInteger(), settingService.getSystem(MusicSetting.MAX_FRAME_LENGTH).asInteger(),
0, 0,
4, 4,
0, 0,
@@ -79,7 +79,7 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
channel = bootstrap.bind().sync().channel(); 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) { } catch (Exception e) {
log.error("TimiMusicRC player service startup error", e); log.error("TimiMusicRC player service startup error", e);
} }
@@ -61,7 +61,7 @@ public class ServerFile implements Serializable {
name = file.getName(); name = file.getName();
absolutePath = file.getAbsolutePath().replaceAll("\\\\", "/"); absolutePath = file.getAbsolutePath().replaceAll("\\\\", "/");
SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class); 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(); modifiedAt = file.lastModified();
this.file = file.isFile(); this.file = file.isFile();
@@ -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;
}
@@ -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;
}
@@ -1,9 +1,8 @@
package com.imyeyu.api.modules.system.controller; 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.SettingService;
import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.api.modules.system.bean.ServerFile; 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.bean.TransferFile;
import com.imyeyu.api.modules.system.service.AsyncTaskService; import com.imyeyu.api.modules.system.service.AsyncTaskService;
import com.imyeyu.api.modules.system.service.FileService; import com.imyeyu.api.modules.system.service.FileService;
@@ -47,7 +46,6 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -67,7 +65,6 @@ import java.util.Map;
@RequestMapping("/system/file") @RequestMapping("/system/file")
public class FileController implements TimiJava, OS.FileSystem { public class FileController implements TimiJava, OS.FileSystem {
private final TagService tagService;
private final FileService service; private final FileService service;
private final SettingService settingService; private final SettingService settingService;
private final AsyncTaskService asyncTaskService; private final AsyncTaskService asyncTaskService;
@@ -143,7 +140,7 @@ public class FileController implements TimiJava, OS.FileSystem {
public void read(HttpServletRequest req, HttpServletResponse resp) { public void read(HttpServletRequest req, HttpServletResponse resp) {
try { try {
String path = req.getServletPath().substring("/system/file/read".length()); 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)) { if (TimiJava.isEmpty(path)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return; return;
@@ -195,8 +192,7 @@ public class FileController implements TimiJava, OS.FileSystem {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return; return;
} }
// path = settingService.getAsString(Setting.Module.System.FILE_BASE_PATH) + path; File file = new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path);
File file = new File(path);
if (!file.exists()) { if (!file.exists()) {
log.warn("preview a not exist file: {}", path); log.warn("preview a not exist file: {}", path);
resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
@@ -409,8 +405,7 @@ public class FileController implements TimiJava, OS.FileSystem {
public void download(HttpServletResponse resp) { public void download(HttpServletResponse resp) {
try { try {
String path = TimiSpring.cutURIStartAt("/system/file/download"); String path = TimiSpring.cutURIStartAt("/system/file/download");
path = "E:/IDEAProject/TimiServerAPI/data/file" + path; File file = new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path);
File file = new File(path);
service.checkAccessPermission(file.getAbsolutePath()); service.checkAccessPermission(file.getAbsolutePath());
String mimeType = new Tika().detect(file); String mimeType = new Tika().detect(file);
@@ -1,8 +1,8 @@
package com.imyeyu.api.modules.system.service.implement; 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.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerFile; 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.bean.TransferFile;
import com.imyeyu.api.modules.system.service.AsyncTaskService; import com.imyeyu.api.modules.system.service.AsyncTaskService;
import com.imyeyu.api.modules.system.service.FileService; import com.imyeyu.api.modules.system.service.FileService;
@@ -257,8 +257,7 @@ public class FileServiceImplement implements TimiJava, FileService {
} }
private String getPath(String path) { private String getPath(String path) {
// String basePath = settingService.getSystem(Setting.Module.System.FILE_BASE_PATH).asString(); String basePath = settingService.getSystem(SystemFileSetting.BASE_PATH).asString();
String basePath = "E:\\IDEAProject\\TimiServerAPI\\data\\file";
if (TimiJava.isEmpty(path)) { if (TimiJava.isEmpty(path)) {
return basePath; return basePath;
} }
@@ -1,8 +1,8 @@
package com.imyeyu.api.modules.system.service.implement; 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.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus; 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.service.StatusService;
import com.imyeyu.api.modules.system.vo.SystemStatusDataView; import com.imyeyu.api.modules.system.vo.SystemStatusDataView;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView; import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
@@ -33,7 +33,7 @@ public class StatusServiceImplement implements StatusService {
@Override @Override
public SystemStatusSnapshotView getStatus(String metrics) { public SystemStatusSnapshotView getStatus(String metrics) {
long serverTime = Time.now(); long serverTime = Time.now();
int sampleRateMs = settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger(); int sampleRateMs = settingService.getSystem(SystemStatusSetting.RATE).asInteger();
EnumSet<Metric> selectedMetrics = parseMetrics(metrics); EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) { synchronized (serverStatus) {
@@ -48,7 +48,7 @@ public class StatusServiceImplement implements StatusService {
@Override @Override
public SystemStatusHistoryView getStatusHistory(String window, String metrics) { public SystemStatusHistoryView getStatusHistory(String window, String metrics) {
long serverTime = Time.now(); long serverTime = Time.now();
int sampleRateMs = settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger(); int sampleRateMs = settingService.getSystem(SystemStatusSetting.RATE).asInteger();
EnumSet<Metric> selectedMetrics = parseMetrics(metrics); EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) { synchronized (serverStatus) {
@@ -261,7 +261,7 @@ public class StatusServiceImplement implements StatusService {
*/ */
private long parseWindowMs(String window, int sampleRateMs) { private long parseWindowMs(String window, int sampleRateMs) {
if (window == null || window.isBlank()) { 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); String normalized = window.trim().toLowerCase(Locale.ROOT);
@@ -277,7 +277,7 @@ public class StatusServiceImplement implements StatusService {
try { try {
return Math.max(sampleRateMs, Long.parseLong(valueText) * unit); return Math.max(sampleRateMs, Long.parseLong(valueText) * unit);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return (long) settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger() * sampleRateMs; return (long) settingService.getSystem(SystemStatusSetting.RATE).asInteger() * sampleRateMs;
} }
} }
@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.system.task.status; 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; import java.util.Deque;
@@ -22,7 +22,7 @@ public abstract class AbstractDequeStatusCollector implements StatusCollector {
*/ */
protected <T> void putDeque(StatusCollectContext context, Deque<T> deque, T value) { protected <T> void putDeque(StatusCollectContext context, Deque<T> deque, T value) {
deque.addLast(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(); deque.pollFirst();
} }
} }
@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.system.task.status.collector; 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.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext; import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -24,7 +24,7 @@ public class NetworkStatusCollector extends AbstractDequeStatusCollector {
@Override @Override
public void initialize(StatusCollectContext context) { public void initialize(StatusCollectContext context) {
List<NetworkIF> networkIFs = context.getHardware().getNetworkIFs(); List<NetworkIF> 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) { for (NetworkIF networkIF : networkIFs) {
if (networkIF.getMacaddr().equals(targetMac)) { if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes(); networkIF.updateAttributes();
@@ -41,8 +41,8 @@ public class NetworkStatusCollector extends AbstractDequeStatusCollector {
@Override @Override
public void collect(StatusCollectContext context) { public void collect(StatusCollectContext context) {
String targetMac = context.getSettingService().getSystem(Setting.Module.SystemStatus.NETWORK_MAC).asString(); String targetMac = context.getSettingService().getSystem(SystemStatusSetting.NETWORK_MAC).asString();
int sampleRateMs = context.getSettingService().getSystem(Setting.Module.SystemStatus.RATE).asInteger(); int sampleRateMs = context.getSettingService().getSystem(SystemStatusSetting.RATE).asInteger();
for (NetworkIF networkIF : context.getHardware().getNetworkIFs()) { for (NetworkIF networkIF : context.getHardware().getNetworkIFs()) {
if (networkIF.getMacaddr().equals(targetMac)) { if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes(); networkIF.updateAttributes();
@@ -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;
}
@@ -3,12 +3,12 @@ package com.imyeyu.api.modules.user.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.NotifyDetail; 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.AttachmentService;
import com.imyeyu.api.modules.common.service.SettingService; import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.SmsService; import com.imyeyu.api.modules.common.service.SmsService;
import com.imyeyu.api.modules.common.vo.SmsCaptchaVerifyRequest; import com.imyeyu.api.modules.common.vo.SmsCaptchaVerifyRequest;
import com.imyeyu.api.modules.user.bean.BootstrapData; 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.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserRoleService; import com.imyeyu.api.modules.user.service.UserRoleService;
@@ -79,7 +79,7 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
@Override @Override
public LoginResponse login(String token, String userId) { 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 now = Time.now();
long expireAt = now + time; long expireAt = now + time;
redisUserToken.set(getUserTokenKey(token), userId, expireAt - now); redisUserToken.set(getUserTokenKey(token), userId, expireAt - now);
@@ -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<SettingEnum> findSettingEnumList() throws ClassNotFoundException {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(SettingEnum.class));
List<BeanDefinition> candidateList = new ArrayList<>(scanner.findCandidateComponents(SCAN_PACKAGE));
List<SettingEnum> 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;
}
}
@@ -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`;
@@ -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 同单位重复由既有唯一键阻止
@@ -52,6 +52,7 @@
<if test="status != null">AND f.`status` = #{status}</if> <if test="status != null">AND f.`status` = #{status}</if>
<if test="beginAt != null">AND f.`occurred_at` &gt;= #{beginAt}</if> <if test="beginAt != null">AND f.`occurred_at` &gt;= #{beginAt}</if>
<if test="endAt != null">AND f.`occurred_at` &lt;= #{endAt}</if> <if test="endAt != null">AND f.`occurred_at` &lt;= #{endAt}</if>
<if test="operatorUserId != null and operatorUserId != ''">AND f.`operator_user_id` = #{operatorUserId}</if>
</sql> </sql>
<select id="countByQuery" resultType="long"> <select id="countByQuery" resultType="long">
SELECT COUNT(1) FROM `gao_fund_record` f WHERE <include refid="queryWhere"/> SELECT COUNT(1) FROM `gao_fund_record` f WHERE <include refid="queryWhere"/>
@@ -103,14 +103,14 @@
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = '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(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.`reserved_base_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`, COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`, 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.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`, 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.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` 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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, COALESCE(SUM(i.`fulfilled_base_quantity`), 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.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`
FROM `gao_sales_order_item` i FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` 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` INNER JOIN `gao_product` p ON p.`id` = i.`product_id_snapshot` AND p.`store_id` = o.`store_id`
@@ -127,9 +127,9 @@
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = '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(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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, COALESCE(SUM(i.`fulfilled_base_quantity`), 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.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`
FROM `gao_sales_order_item` i FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` 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` INNER JOIN `gao_product` p ON p.`id` = i.`product_id_snapshot` AND p.`store_id` = o.`store_id`
@@ -86,14 +86,14 @@
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = '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(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.`reserved_base_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`, COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`, 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.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`, 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.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` 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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, COALESCE(SUM(i.`fulfilled_base_quantity`), 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.`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` 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 FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id`
@@ -124,11 +124,11 @@
i.`product_id_snapshot` AS `productId`, i.`product_id_snapshot` AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`, MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = 'DIRECT_PRODUCT' THEN i.`base_quantity` 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(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` 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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`, COALESCE(SUM(i.`fulfilled_base_quantity`), 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.`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` 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 FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id`
@@ -20,16 +20,16 @@
WHERE `id` = #{id} AND `deleted_at` IS NULL WHERE `id` = #{id} AND `deleted_at` IS NULL
FOR UPDATE FOR UPDATE
</select> </select>
<update id="updateReservedQuantity"> <update id="updateReservedBaseQuantity">
UPDATE `gao_sales_order_item` UPDATE `gao_sales_order_item`
SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) SET `reserved_base_quantity` = `reserved_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `reserved_quantity` + #{delta} &gt;= 0 WHERE `id` = #{id} AND `reserved_base_quantity` + #{delta} &gt;= 0
AND `fulfilled_quantity` + `reserved_quantity` + #{delta} &lt;= `quantity` AND `fulfilled_base_quantity` + `reserved_base_quantity` + #{delta} &lt;= `base_quantity`
</update> </update>
<update id="updateFulfilledQuantity"> <update id="updateFulfilledBaseQuantity">
UPDATE `gao_sales_order_item` UPDATE `gao_sales_order_item`
SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) SET `fulfilled_base_quantity` = `fulfilled_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `fulfilled_quantity` + #{delta} &gt;= 0 WHERE `id` = #{id} AND `fulfilled_base_quantity` + #{delta} &gt;= 0
AND `fulfilled_quantity` + #{delta} &lt;= `quantity` AND `fulfilled_base_quantity` + #{delta} &lt;= `base_quantity`
</update> </update>
</mapper> </mapper>