Merge pull request 'v1.0.36' (#48) from dev into master

Reviewed-on: #48
This commit was merged in pull request #48.
This commit is contained in:
2026-09-08 17:52:47 +00:00
50 changed files with 752 additions and 725 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>
<artifactId>TimiServerAPI</artifactId>
<version>1.0.35</version>
<version>1.0.36</version>
<packaging>jar</packaging>
<name>TimiServerAPI</name>
<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.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.api.annotation.EnableSettingInterceptor;
import com.imyeyu.api.annotation.RequestRateLimitInterceptor;
import com.imyeyu.api.annotation.RequireCorePermissionInterceptor;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor;
@@ -40,7 +39,6 @@ public class WebConfig implements WebMvcConfigurer {
private final ObjectMapper jackson;
private final SystemAPIInterceptor systemAPIInterceptor;
private final JournalAPIInterceptor journalAPIInterceptor;
private final EnableSettingInterceptor enableSettingInterceptor;
private final RequestRateLimitInterceptor requestRateLimitInterceptor;
private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor;
private final RequireCorePermissionInterceptor requireCorePermissionInterceptor;
@@ -54,7 +52,6 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH);
registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH);
registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**");
@@ -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
@PostMapping("/create")
public Notify create(@RequestBody Notify notify) {
String sendFrom = userLoginService.isLogged() ? userLoginService.getRequireLoginUserId() : null;
notify.setCreatedBy(sendFrom);
for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId());
if (TimiJava.isNotEmpty(sendFrom)) {
detail.setSendFrom(sendFrom);
}
}
service.create(notify);
return notify;
}
@@ -53,6 +53,7 @@ public class SettingController {
*/
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_READ)
@PostMapping("/detail")
public Setting detail(@RequestBody String id) {
return service.get(id);
@@ -66,6 +67,7 @@ public class SettingController {
*/
@AOPLog
@JsonView(ResponseView.Admin.class)
@RequireCorePermission(CorePermissionCode.SETTING_CREATE)
@RequestRateLimit
@PostMapping("/create")
public Setting create(@RequestBody Setting setting) {
@@ -80,6 +82,7 @@ public class SettingController {
*/
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_UPDATE)
@PostMapping("/update")
public void update(@RequestBody Setting setting) {
service.update(setting);
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -39,4 +40,7 @@ public class GroovyScript extends UUIDEntity {
/// 当前发布版本 ID
private String publishedVersionId;
@Transient
private GroovyScriptVersion latest;
}
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.SettingModule;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import com.imyeyu.spring.util.ResponseView;
@@ -62,6 +61,9 @@ public class Setting extends UUIDEntity {
/** 时间 */
TIME,
/// 字节大小
BYTE_SIZE,
/** 时长 */
DURATION,
@@ -102,16 +104,6 @@ public class Setting extends UUIDEntity {
@Transient
private Enum<?> keyRaw;
public Setting(Enum<?> key) {
setKeyRaw(key);
}
public Setting(Enum<?> key, Object value, ValueType valueType) {
setKeyRaw(key);
this.value = value.toString();
this.valueType = valueType;
}
/**
* 以字符串形式返回配置值
*
@@ -221,298 +213,4 @@ public class Setting extends UUIDEntity {
throw new IllegalArgumentException("setting value is not valid json: %s".formatted(value), e);
}
}
public void setKeyRaw(Enum<?> key) {
this.keyRaw = key;
this.bizType = SettingModule.fromClass(key.getClass()).toString();
this.code = key.toString();
}
public boolean is(Enum<?> key) {
return SettingModule.fromClass(key.getClass()).toString().equals(bizType) && key.toString().equals(code);
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public static final class Module {
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:39
*/
public enum Common {
/** 微信收款码 */
WECHAT_RECEIVE_QR_CODE
}
/**
* 域名
*
* @author 夜雨
* @since 2026-04-26 17:18
*/
public enum Domain {
ROOT,
API,
GIT,
BLOG,
SPACE,
FOREVER_MC
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:14
*/
public enum Blog {
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:03
*/
public enum User {
/** 启用登录 */
ENABLE_LOGIN,
/** 启用注册 */
ENABLE_REGISTER,
/** 允许数据更新 */
ENABLE_PROFILE_UPDATE,
/** 令牌有效期 */
TOKEN_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:21
*/
public enum Comment {
/** 启用服务 */
ENABLE
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:03
*/
public enum ICP {
IMYEYU_COM
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:01
*/
public enum ForeverMC {
PLAYER_BIND_MAX,
LOGIN_TOKEN_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:57
*/
public enum Multilingual {
/** 翻译 API */
TRANSLATE_API,
/** 翻译 AppId */
TRANSLATE_APP_ID,
/** 翻译密钥 */
TRANSLATE_KEY,
/** 翻译计划任务表达式 */
TRANSLATE_CORN,
/** 缓存时长 */
CACHE_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:55
*/
public enum Gitea {
/** 接口地址 */
API,
/** 关于页面所属文章 ID */
ABOUT_ARTICLE_ID,
/** 仓库文件地址 */
REPOSITORY_PATH
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:52
*/
public enum Music {
/** 启用服务 */
ENABLE,
/** 通信消息最大帧长度 */
MAX_FRAME_LENGTH,
/** 受控端 IP */
PLAYER_IP,
/** 受控端端口 */
PLAYER_PORT,
/** 控制端 IP */
CONTROLLER_IP,
/** 控制端端口 */
CONTROLLER_PORT,
/** 控制端入口地址 */
CONTROLLER_URI,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:49
*/
public enum Journal {
/** 启用服务 */
ENABLE,
/** 审核中 */
REVIEWING,
/** 访问密钥 */
KEY,
/** 微信小程序 AppId */
APP_ID,
/** 微信小程序 AppSecret */
APP_SECRET,
/** 备忘录内容 */
MEMO,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:47
*/
public enum TempFile {
/** 启用服务 */
ENABLE,
/** 最小缓存时长 */
TTL_MIN,
/** 默认缓存时长 */
TTL_DEFAULT,
/** 最大缓存时长 */
TTL_MAX,
/** 已过期文件残留时长 */
RESIDUE_TTL,
/** 每个 IP 限制有效临时文件容量 */
LIMIT_SIZE_OF_IP
}
/**
*
*
* @author 夜雨
* @since 2026-04-27 14:35
*/
public enum FileSync {
ENABLE,
CRON,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public enum System {
/** 文件系统基准路径 */
FILE_BASE_PATH,
/** 设置缓存时长 */
SETTING_TTL,
/** 哀悼滤镜 */
ENABLE_MOURN_FILTER,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public enum SystemStatus {
/** 状态采集数据量 */
LIMIT,
/** 状态采集网卡 */
NETWORK_MAC,
/** 状态采集频率 */
RATE,
}
}
}
@@ -2,10 +2,14 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/// Groovy 脚本版本 Mapper
///
/// @author Codex
/// @since 2026-09-04
public interface GroovyScriptVersionMapper extends BaseMapper<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;
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
import com.imyeyu.api.modules.common.entity.GroovyScript;
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
@@ -16,6 +16,12 @@ import java.util.Map;
/// @since 2026-09-04
public interface GroovyScriptService extends BaseService<GroovyScript, String> {
/// 按业务归属查询脚本列表
///
/// @param bizType 业务归属
/// @return 脚本列表
List<GroovyScript> listByBizType(GroovyScript.BizType bizType);
/// 按业务归属和代码查询脚本
///
/// @param bizType 业务归属
@@ -23,26 +29,14 @@ public interface GroovyScriptService extends BaseService<GroovyScript, String> {
/// @return 脚本,不存在时返回 null
GroovyScript getByCode(GroovyScript.BizType bizType, String code);
/// 创建脚本元数据
///
/// 新建脚本默认停用,且没有已发布版本
///
/// @param script 脚本元数据
void create(GroovyScript script);
/// 更新脚本元数据
///
/// 业务归属和代码是脚本稳定标识,创建后不可变更
///
/// @param script 脚本元数据
void update(GroovyScript script);
/// 设置脚本启用状态
///
/// @param id 脚本 ID
/// @param enabled true 为允许执行已发布版本
void setEnabled(String id, boolean enabled);
GroovyScriptVersion getLatestVersion(String scriptId);
/// 创建不可变的脚本版本
///
/// @param scriptId 脚本 ID
@@ -67,11 +61,6 @@ public interface GroovyScriptService extends BaseService<GroovyScript, String> {
/// @param scriptId 脚本 ID
void unpublish(String scriptId);
/// 删除脚本
///
/// @param id 脚本 ID
void delete(String id);
/// 执行已发布且启用的脚本
///
/// @param bizType 业务归属
@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.bean.SettingModule;
import com.imyeyu.api.bean.SettingEnum;
import com.imyeyu.api.modules.common.bean.attachment.SettingSupport;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.java.TimiJava;
@@ -18,8 +18,8 @@ public interface SettingService extends BaseService<Setting, String> {
Setting getByCode(String bizType, String bizId, String code);
default Setting getSystem(Enum<?> bizType) {
return getByCode(SettingModule.fromClass(bizType.getClass()).name(), TimiJava.ZERO_UUID, bizType.toString());
default Setting getSystem(SettingEnum setting) {
return getByCode(setting.getBizType(), TimiJava.ZERO_UUID, setting.name());
}
List<Setting> listByBizId(String bizType, String bizId);
@@ -1,9 +1,9 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
import com.imyeyu.api.modules.common.entity.GroovyScript;
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
import com.imyeyu.api.modules.common.mapper.GroovyScriptMapper;
import com.imyeyu.api.modules.common.mapper.GroovyScriptVersionMapper;
import com.imyeyu.api.modules.common.service.GroovyScriptService;
@@ -52,6 +52,14 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
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
public GroovyScript getByCode(GroovyScript.BizType bizType, String code) {
GroovyScript example = new GroovyScript();
@@ -95,6 +103,11 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
invalidateAfterCommit(script.getId());
}
@Override
public GroovyScriptVersion getLatestVersion(String scriptId) {
return versionMapper.selectLatest(scriptId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GroovyScriptVersion createVersion(String scriptId, String content) {
@@ -104,7 +117,7 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
GroovyScriptVersion version = new GroovyScriptVersion();
version.setScriptId(scriptId);
version.setVersion(nextVersion(scriptId));
version.setVersion(versionMapper.selectLatest(scriptId).getVersion() + 1);
version.setContent(content);
version.setChecksum(checksum(content));
versionMapper.insert(version);
@@ -218,13 +231,6 @@ public class GroovyScriptServiceImplement extends AbstractEntityService<GroovySc
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) {
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.service.NotifyDetailService;
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.service.LoggerService;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
@@ -61,15 +61,13 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
try {
TimiException.required(notify, "未找到通知");
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);
for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId());
if (TimiJava.isNotEmpty(sendFrom)) {
detail.setSendFrom(sendFrom);
}
detail.setSendFrom(TimiJava.defaultIfEmpty(detail.getSendFrom(), TimiJava.ZERO_UUID));
detailService.create(detail);
}
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.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.common.bean.TempFileSetting;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.AttachmentService;
@@ -52,11 +53,11 @@ public class TempFileServiceImplement implements TempFileService {
try {
logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl)));
Setting ttlMin = settingService.getSystem(Setting.Module.TempFile.TTL_MIN);
Setting ttlMax = settingService.getSystem(Setting.Module.TempFile.TTL_MAX);
Setting ttlDefault = settingService.getSystem(Setting.Module.TempFile.TTL_DEFAULT);
Setting residueTime = settingService.getSystem(Setting.Module.TempFile.RESIDUE_TTL);
String limitStr = settingService.getSystem(Setting.Module.TempFile.LIMIT_SIZE_OF_IP).asString();
Setting ttlMin = settingService.getSystem(TempFileSetting.TTL_MIN);
Setting ttlMax = settingService.getSystem(TempFileSetting.TTL_MAX);
Setting ttlDefault = settingService.getSystem(TempFileSetting.TTL_DEFAULT);
Setting residueTime = settingService.getSystem(TempFileSetting.RESIDUE_TTL);
String limitStr = settingService.getSystem(TempFileSetting.LIMIT_SIZE_OF_IP).asString();
long limit = IOSize.parse(limitStr);
@@ -1,5 +1,7 @@
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.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
@@ -31,6 +33,7 @@ public class GaoPointRuleController {
private final GaoStoreScopeService storeScopeService;
private final GaoPointRuleService ruleService;
private final GroovyScriptService groovyScriptService;
@AOPLog
@RequestRateLimit
@@ -54,9 +57,22 @@ public class GaoPointRuleController {
GaoPointRule rule = ruleService.get(id);
TimiException.required(rule, "not found point rule");
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;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ)
@PostMapping("/rule/script/list")
public List<GroovyScript> scriptList() {
return groovyScriptService.listByBizType(GroovyScript.BizType.GAO_POINT_RULE);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE)
@@ -29,7 +29,7 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// SKU ID
private String skuId;
/// SKU 单位 ID
/// 本次实际履约 SKU 单位 ID
private String skuUnitId;
/// 产品图片附件
@@ -45,17 +45,17 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// 基础单位数量
private BigDecimal baseQuantity;
/// 操作前待履约数量
private BigDecimal reservedBefore;
/// 操作前待履约基础单位数量
private BigDecimal reservedBaseBefore;
/// 操作后待履约数量
private BigDecimal reservedAfter;
/// 操作后待履约基础单位数量
private BigDecimal reservedBaseAfter;
/// 操作前已履约数量
private BigDecimal fulfilledBefore;
/// 操作前已履约基础单位数量
private BigDecimal fulfilledBaseBefore;
/// 操作后已履约数量
private BigDecimal fulfilledAfter;
/// 操作后已履约基础单位数量
private BigDecimal fulfilledBaseAfter;
/// 产品名称快照
private String productNameSnapshot;
@@ -1,5 +1,7 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.common.entity.GroovyScript;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -96,4 +98,7 @@ public class GaoPointRule extends UUIDEntity {
/// 备注
private String remark;
@Transient
private GroovyScript groovyScript;
}
@@ -71,11 +71,14 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 购买数量
private BigDecimal quantity;
/// 待履约数量
private BigDecimal reservedQuantity;
/// 购买基础单位数量
private BigDecimal baseQuantity;
/// 已履约数量
private BigDecimal fulfilledQuantity;
/// 待履约基础单位数量
private BigDecimal reservedBaseQuantity;
/// 已履约基础单位数量
private BigDecimal fulfilledBaseQuantity;
/// 实际单价,单位分
private Long unitPrice;
@@ -89,15 +92,15 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 备注
private String remark;
/// 待核销数量
/// 待核销基础单位数量
@Transient
private BigDecimal pendingQuantity;
private BigDecimal pendingBaseQuantity;
/// 当前可履约数量
/// 当前可履约基础单位数量
@Transient
private BigDecimal availableQuantity;
private BigDecimal availableBaseQuantity;
/// 权益剩余数量
/// 权益剩余基础单位数量
@Transient
private BigDecimal remainingQuantity;
private BigDecimal remainingBaseQuantity;
}
@@ -22,9 +22,9 @@ public interface GaoSalesOrderItemMapper extends BaseMapper<GaoSalesOrderItem, S
/// 锁定订单明细
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);
}
@@ -58,11 +58,11 @@ public interface GaoSalesOrderService extends BaseService<GaoSalesOrder, String>
/// 检查履约权限
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);
@@ -22,6 +22,7 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem;
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
@@ -38,6 +39,10 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -53,6 +58,8 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class GaoCustomerPackageServiceImplement extends AbstractEntityService<GaoCustomerPackage, String> implements GaoCustomerPackageService {
private static final BigDecimal ZERO = BigDecimal.ZERO;
private final GaoCustomerPackageMapper mapper;
private final GaoPackageConfigService packageConfigService;
private final GaoProductSkuService skuService;
@@ -143,9 +150,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
if (request.getItemList() == null) {
request.setItemList(defaultPurchaseItems(configItemList));
}
List<GaoCustomerPackagePurchaseItem> itemList = new java.util.ArrayList<>(request.getItemList());
verifyPurchaseItems(itemList);
itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort));
List<GaoCustomerPackagePurchaseItem> requestItemList = new ArrayList<>(request.getItemList());
verifyPurchaseItems(requestItemList);
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO,
com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
List<GaoCustomerPackagePurchaseItem> itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList, canOverridePriceAndQuantity);
itemList.sort(Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort).thenComparing(GaoCustomerPackagePurchaseItem::getSkuId)
.thenComparing(GaoCustomerPackagePurchaseItem::getSkuUnitId));
request.setItemList(itemList);
GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo());
@@ -154,19 +165,14 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
verifyPurchaseIdempotency(existingOrder, existingPackage, request);
return existingPackage;
}
verifyConfigSellableItems(config.getStoreId(), configItemList);
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
Long now = Time.now();
TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买");
TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效");
TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期");
GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId());
verifySameStructureAsConfig(itemList, configItemList);
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO, com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
if (!canOverridePriceAndQuantity) {
TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价");
verifySameQuantityAsConfig(itemList, configItemList);
}
List<GaoSalesOrderItem> orderItemList = itemList.stream().map(this::toOrderItem).toList();
@@ -267,8 +273,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
item.setUnitSnapshot(source.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
item.setQuantity(source.getTotalQuantity());
item.setReservedQuantity(BigDecimal.ZERO);
item.setFulfilledQuantity(BigDecimal.ZERO);
item.setBaseQuantity(toBaseQuantity(source.getTotalQuantity(), source.getBaseQuantityPerUnitSnapshot(), "套餐权益基础数量最多保留三位小数"));
item.setReservedBaseQuantity(ZERO);
item.setFulfilledBaseQuantity(ZERO);
item.setUnitPrice(null);
item.setLineAmount(null);
item.setSort(source.getSort());
@@ -277,7 +284,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
}
private void verifyPurchaseItems(List<GaoCustomerPackagePurchaseItem> itemList) {
TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品");
Set<String> keySet = new HashSet<>();
for (int i = 0; i < itemList.size(); i++) {
GaoCustomerPackagePurchaseItem item = itemList.get(i);
@@ -285,7 +291,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
TimiException.required(item.getSkuId(), "购买 SKU 不能为空");
TimiException.required(item.getSkuUnitId(), "购买 SKU 单位不能为空");
TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空");
TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0");
TimiException.requiredTrue(0 <= item.getTotalQuantity().signum(), "购买产品数量不能为负数");
TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
String key = item.getSkuId() + "|" + item.getSkuUnitId();
TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
@@ -293,38 +299,69 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
}
}
private void verifyConfigSellableItems(String storeId, List<GaoPackageConfigItem> itemList) {
for (GaoPackageConfigItem item : itemList) {
TimiException.required(item.getSkuId(), "套餐配置缺少 SKU");
TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位");
// 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态
skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId());
private List<GaoCustomerPackagePurchaseItem> resolvePurchaseItems(String storeId, List<GaoCustomerPackagePurchaseItem> requestItemList,
List<GaoPackageConfigItem> configItemList, boolean canOverridePriceAndQuantity) {
Map<String, List<GaoPackageConfigItem>> configItemMap = configItemList.stream()
.collect(Collectors.groupingBy(GaoPackageConfigItem::getSkuId));
Map<String, BigDecimal> configBaseQuantityMap = new HashMap<>();
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) {
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致");
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId());
TimiException.required(item, "套餐权益项目必须与套餐配置一致");
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
item.setProductIdSnapshot(configItem.getProductIdSnapshot());
item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot());
item.setSkuNameSnapshot(configItem.getSkuNameSnapshot());
item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot());
item.setUnitSnapshot(configItem.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
&& sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改");
}
private GaoPackageConfigItem resolveItemTemplate(List<GaoPackageConfigItem> configItemList, String skuUnitId) {
return configItemList.stream().filter(item -> skuUnitId.equals(item.getSkuUnitId())).findFirst()
.orElseGet(() -> configItemList.stream().min(Comparator.comparing(GaoPackageConfigItem::getSort)).orElseThrow());
}
private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId());
TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额");
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
BigDecimal result = quantity.multiply(baseQuantityPerUnit);
TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
try {
return result.setScale(3, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, message, e);
}
}
@@ -29,6 +29,7 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
@@ -46,6 +47,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@@ -194,12 +196,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment");
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);
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存");
salesOrderService.checkFulfillmentPermission(order);
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
@@ -228,12 +229,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment");
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);
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约");
salesOrderService.checkFulfillmentPermission(order);
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
assertReserved(sourceList);
@@ -283,7 +283,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment");
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);
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK
@@ -326,7 +326,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
}
GaoSalesOrder order = salesOrderService.getForUpdate(sourceSnapshot.getSalesOrderId());
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);
storeScopeService.checkStoreId(source.getStoreId());
TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正");
@@ -356,10 +356,10 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
TimiException.required(sourceItem, "履约明细来源订单项不存在");
GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem);
reverseItem.setFulfillmentId(reverse.getId());
reverseItem.setReservedBefore(sourceEntry.reservedBefore());
reverseItem.setReservedAfter(sourceEntry.reservedBefore());
reverseItem.setFulfilledBefore(sourceEntry.fulfilledBefore());
reverseItem.setFulfilledAfter(sourceEntry.fulfilledBefore().subtract(sourceEntry.quantity()));
reverseItem.setReservedBaseBefore(sourceEntry.reservedBaseBefore());
reverseItem.setReservedBaseAfter(sourceEntry.reservedBaseBefore());
reverseItem.setFulfilledBaseBefore(sourceEntry.fulfilledBaseBefore());
reverseItem.setFulfilledBaseAfter(sourceEntry.fulfilledBaseBefore().subtract(sourceEntry.baseQuantity()));
itemMapper.insert(reverseItem);
}
if (!stockItems.isEmpty()) {
@@ -397,28 +397,28 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private void insertSourceReservations(GaoOrderFulfillment fulfillment, List<SourceEntry> sourceList) {
for (SourceEntry source : sourceList) {
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "订单明细占用失败");
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细占用失败");
GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId());
itemMapper.insert(item);
}
}
private void decreaseReserved(SourceEntry source) {
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "订单明细占用释放失败");
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细占用释放失败");
}
private void increaseFulfilled(SourceEntry source) {
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "订单明细履约失败");
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细履约失败");
}
private void decreaseFulfilled(SourceEntry source) {
TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正");
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败");
TimiException.requiredTrue(0 <= source.fulfilledBaseBefore().compareTo(source.baseQuantity()), "已履约数量不足以冲正");
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细冲正失败");
}
private void assertReserved(List<SourceEntry> 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, "履约明细不能为空");
String sourceId = requiredText(requestItem.getSourceOrderItemId(), "履约明细来源订单项不能为空");
requestItem.setSourceOrderItemId(sourceId);
requestItem.setSkuUnitId(requiredText(requestItem.getSkuUnitId(), "履约 SKU 单位不能为空"));
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
TimiException.required(requestItem.getQuantity(), "履约数量不能为空");
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.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
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;
}
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) {
private List<SourceEntry> loadPersistedSources(GaoSalesOrder order, List<GaoOrderFulfillmentItem> itemList) {
TimiException.requiredTrue(itemList != null && !itemList.isEmpty(), "履约单没有明细");
List<GaoOrderFulfillmentItem> sortedItemList = new ArrayList<>(itemList);
sortedItemList.sort(Comparator.comparing(GaoOrderFulfillmentItem::getSourceOrderItemId));
Set<String> sourceIdSet = new HashSet<>();
List<SourceEntry> result = new ArrayList<>();
for (GaoOrderFulfillmentItem item : sortedItemList) {
TimiException.required(item, "履约明细不能为空");
String sourceId = requiredText(item.getSourceOrderItemId(), "履约明细来源订单项不能为空");
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
GaoSalesOrderItem source = salesOrderService.getItemForUpdate(sourceId);
validateSourceOrderItem(order, source);
GaoProductSku sku = skuService.get(source.getSkuId());
TimiException.required(sku, "履约来源 SKU 不存在");
GaoProduct product = productService.get(sku.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot());
GaoProductSkuUnit unit = snapshotUnit(source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit);
JsonNode skuSpecSnapshot = source.getSkuSpecSnapshot();
return buildSource(order, source.getId(), source.getSkuId(), source.getSkuUnitId(), source.getProductIdSnapshot(), source.getProductNameSnapshot(), source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), skuSpecSnapshot,
source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(),
product, sku, unit, checkAvailable);
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(item.getBaseQuantityPerUnitSnapshot());
BigDecimal quantity = requiredPositiveQuantity(item.getQuantity(), "履约数量");
BigDecimal baseQuantity = requiredBaseQuantity(item.getBaseQuantity());
TimiException.requiredTrue(baseQuantity.compareTo(toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数")) == 0,
"履约基础数量与单位换算快照不一致");
result.add(buildSource(order, source, item.getSkuUnitId(), item.getUnitSnapshot(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, false));
}
return result;
}
private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String skuId, String unitId, String productId,
String productName, String skuCode, String skuName, String specSummary, JsonNode skuSpecSnapshot, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity,
BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit,
boolean checkAvailable) {
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, GaoProduct product, GaoProductSku sku,
GaoProductSkuUnit unit, BigDecimal quantity, boolean checkAvailable) {
TimiException.requiredTrue(source.getSkuId().equals(sku.getId()), "履约单位不属于来源 SKU");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
quantity = requiredPositiveQuantity(quantity, "履约数量");
if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
}
BigDecimal reservedBefore = decimal(reservedQuantity);
BigDecimal fulfilledBefore = decimal(fulfilledQuantity);
if (checkAvailable) {
BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore);
TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足");
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(unit.getBaseQuantityPerUnit());
BigDecimal baseQuantity = toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数");
return buildSource(order, source, unit.getId(), unit.getName(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, checkAvailable);
}
return new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, skuSpecSnapshot, unitName, baseQuantityPerUnit,
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit);
private SourceEntry buildSource(GaoSalesOrder order, GaoSalesOrderItem source, String unitId, String unitName,
BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal baseQuantity, GaoProduct product, GaoProductSku sku,
boolean checkAvailable) {
BigDecimal reservedBaseBefore = decimal(source.getReservedBaseQuantity());
BigDecimal fulfilledBaseBefore = decimal(source.getFulfilledBaseQuantity());
BigDecimal totalBaseQuantity = requiredBaseQuantity(source.getBaseQuantity());
if (checkAvailable) {
BigDecimal availableBaseQuantity = totalBaseQuantity.subtract(reservedBaseBefore).subtract(fulfilledBaseBefore);
TimiException.requiredTrue(0 <= availableBaseQuantity.compareTo(baseQuantity), "来源产品可履约数量不足");
}
return new SourceEntry(source.getId(), source.getSkuId(), unitId, source.getProductIdSnapshot(), source.getProductNameSnapshot(),
source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), source.getSkuSpecSnapshot(), unitName,
baseQuantityPerUnit, quantity, baseQuantity, totalBaseQuantity, reservedBaseBefore, fulfilledBaseBefore, product, sku);
}
private void validateSourceOrderItem(GaoSalesOrder order, GaoSalesOrderItem source) {
TimiException.required(source, "not found sales order item");
TimiException.requiredTrue(order.getId().equals(source.getSalesOrderId()), "订单明细不属于当前订单");
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
}
private List<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()
.filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null);
TimiException.required(baseUnit, "实物产品缺少基础库存单位");
BigDecimal baseQuantity = source.quantity().multiply(source.unit().getBaseQuantityPerUnit());
BigDecimal baseQuantity = source.baseQuantity();
GaoStockChangeItem item = itemMap.get(source.sku().getId());
if (item == null) {
item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity);
@@ -528,21 +562,6 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<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) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setSkuId(source.getSkuId());
@@ -561,20 +580,36 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
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) {
TimiException.required(value, "履约来源缺少基础单位数量系数快照");
TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效");
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) {
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
&& (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<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList();
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)) {
requestMap.put(item.getSourceOrderItemId(), item.getQuantity());
requestMap.put(item.getSourceOrderItemId(), item);
}
for (GaoOrderFulfillmentItem item : existingItems) {
TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getSourceOrderItemId(), ZERO)) == 0, "requestNo 对应履约请求内容不一致");
GaoOrderFulfillmentItemRequest requestItem = requestMap.get(item.getSourceOrderItemId());
TimiException.requiredTrue(requestItem != null && item.getSkuUnitId().equals(requestItem.getSkuUnitId())
&& item.getQuantity().compareTo(requestItem.getQuantity()) == 0, "requestNo 对应履约请求内容不一致");
}
}
@@ -671,8 +708,8 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot,
String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, JsonNode skuSpecSnapshot,
String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity,
BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, GaoProductSku sku,
GaoProductSkuUnit unit) {
BigDecimal baseQuantity, BigDecimal totalBaseQuantity, BigDecimal reservedBaseBefore, BigDecimal fulfilledBaseBefore,
GaoProduct product, GaoProductSku sku) {
private String sourceId() {
return sourceOrderItemId;
@@ -686,11 +723,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
result.setProductIdSnapshot(productIdSnapshot);
result.setSourceOrderItemId(sourceOrderItemId);
result.setQuantity(quantity);
result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3));
result.setReservedBefore(reservedBefore);
result.setReservedAfter(reservedBefore.add(quantity));
result.setFulfilledBefore(fulfilledBefore);
result.setFulfilledAfter(fulfilledBefore);
result.setBaseQuantity(baseQuantity);
result.setReservedBaseBefore(reservedBaseBefore);
result.setReservedBaseAfter(reservedBaseBefore.add(baseQuantity));
result.setFulfilledBaseBefore(fulfilledBaseBefore);
result.setFulfilledBaseAfter(fulfilledBaseBefore);
result.setProductNameSnapshot(productNameSnapshot);
result.setSkuCodeSnapshot(skuCodeSnapshot);
result.setSkuNameSnapshot(skuNameSnapshot);
@@ -73,6 +73,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
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 GaoSalesOrderItemMapper itemMapper;
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())) {
return;
}
TimiException.requiredTrue(0 < safe(order.getPaidAmount()), "订单未有已核销收款,不能履约");
if (ALLOW_STORE_MANAGER_ZERO_PAYMENT_FULFILLMENT) {
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
return;
}
TimiException.requiredTrue(false, "订单未全额收款,不能发起履约");
}
@Override
public int updateItemReservedQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateReservedQuantity(itemId, delta);
public int updateItemReservedBaseQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateReservedBaseQuantity(itemId, delta);
}
@Override
public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateFulfilledQuantity(itemId, delta);
public int updateItemFulfilledBaseQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateFulfilledBaseQuantity(itemId, delta);
}
@Override
@@ -309,7 +315,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId());
boolean fulfilled = !itemList.isEmpty();
for (GaoSalesOrderItem item : itemList) {
if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) {
if (decimal(item.getFulfilledBaseQuantity()).compareTo(decimal(item.getBaseQuantity())) < 0) {
fulfilled = false;
break;
}
@@ -383,8 +389,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
item.setUnitSnapshot(unit.getName());
item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit());
item.setQuantity(quantity);
item.setReservedQuantity(ZERO);
item.setFulfilledQuantity(ZERO);
item.setBaseQuantity(toBaseQuantity(quantity, unit.getBaseQuantityPerUnit(), "订单项基础单位数量最多保留三位小数"));
item.setReservedBaseQuantity(ZERO);
item.setFulfilledBaseQuantity(ZERO);
item.setUnitPrice(unitPrice);
item.setLineAmount(lineAmount);
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.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空");
TimiException.requiredTrue(0 < item.getBaseQuantityPerUnitSnapshot().signum(), "订单项单位换算快照必须大于 0");
TimiException.requiredTrue(item.getReservedQuantity() != null && item.getFulfilledQuantity() != null,
TimiException.required(item.getBaseQuantity(), "订单项基础单位数量不能为空");
TimiException.requiredTrue(item.getBaseQuantity().compareTo(toBaseQuantity(item.getQuantity(), item.getBaseQuantityPerUnitSnapshot(), "订单项基础单位数量最多保留三位小数")) == 0,
"订单项基础单位数量与单位换算快照不一致");
TimiException.requiredTrue(item.getReservedBaseQuantity() != null && item.getFulfilledBaseQuantity() != null,
"订单项履约数量不能为空");
TimiException.requiredTrue(0 <= item.getReservedQuantity().signum() && 0 <= item.getFulfilledQuantity().signum(),
TimiException.requiredTrue(0 <= item.getReservedBaseQuantity().signum() && 0 <= item.getFulfilledBaseQuantity().signum(),
"订单项履约数量不能为负数");
TimiException.requiredTrue(item.getReservedQuantity().add(item.getFulfilledQuantity()).compareTo(item.getQuantity()) <= 0,
TimiException.requiredTrue(item.getReservedBaseQuantity().add(item.getFulfilledBaseQuantity()).compareTo(item.getBaseQuantity()) <= 0,
"订单项履约数量不能超过购买数量");
if (expectedItemType == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT) {
TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价");
@@ -544,12 +554,12 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
///
/// @param item 订单项
private void fillItemBalance(GaoSalesOrderItem item) {
BigDecimal quantity = decimal(item.getQuantity());
BigDecimal reserved = decimal(item.getReservedQuantity());
BigDecimal fulfilled = decimal(item.getFulfilledQuantity());
item.setPendingQuantity(reserved);
item.setAvailableQuantity(quantity.subtract(reserved).subtract(fulfilled));
item.setRemainingQuantity(quantity.subtract(fulfilled));
BigDecimal baseQuantity = decimal(item.getBaseQuantity());
BigDecimal reservedBaseQuantity = decimal(item.getReservedBaseQuantity());
BigDecimal fulfilledBaseQuantity = decimal(item.getFulfilledBaseQuantity());
item.setPendingBaseQuantity(reservedBaseQuantity);
item.setAvailableBaseQuantity(baseQuantity.subtract(reservedBaseQuantity).subtract(fulfilledBaseQuantity));
item.setRemainingBaseQuantity(baseQuantity.subtract(fulfilledBaseQuantity));
}
private void fillItemAttachments(List<GaoSalesOrderItem> itemList) {
@@ -606,6 +616,24 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
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) {
try {
return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact();
@@ -14,6 +14,9 @@ public class GaoOrderFulfillmentItemRequest {
/// 来源销售订单明细 ID
private String sourceOrderItemId;
/// 本次实际履约 SKU 单位 ID
private String skuUnitId;
/// 本次履约数量
private BigDecimal quantity;
}
@@ -25,14 +25,15 @@ public class ToolController {
@GetMapping("/memo")
public String getMemo() {
return settingService.getSystem(Setting.Module.Journal.MEMO).asString();
// return settingService.getSystem(Setting.Module.Journal.MEMO).asString();
return null;
}
@RequiredUploadPermission
@PostMapping("/memo/update")
public void updateMemo(@RequestBodyValue String data) {
Setting setting = settingService.getSystem(Setting.Module.Journal.MEMO);
setting.setValue(data);
settingService.update(setting);
// Setting setting = settingService.getSystem(Setting.Module.Journal.MEMO);
// setting.setValue(data);
// settingService.update(setting);
}
}
@@ -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;
import com.imyeyu.api.config.CORSConfig;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.music.bean.MusicSetting;
import com.imyeyu.api.modules.music.handler.ControllerMessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
@@ -62,7 +62,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
}
public void run(ApplicationArguments args) {
if (settingService.getSystem(Setting.Module.Music.ENABLE).isTrue()) {
if (settingService.getSystem(MusicSetting.ENABLE).isTrue()) {
return;
}
@@ -75,7 +75,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(Setting.Module.Music.CONTROLLER_PORT).asInteger()));
bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(MusicSetting.CONTROLLER_PORT).asInteger()));
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
@@ -89,7 +89,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
@Override
public void channelRead(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof FullHttpRequest req) {
if (!req.uri().equals(settingService.getSystem(Setting.Module.Music.CONTROLLER_URI).asString())) {
if (!req.uri().equals(settingService.getSystem(MusicSetting.CONTROLLER_URI).asString())) {
// 访问的路径不是 Web Socket 的端点地址,响应 404
ctx.channel().writeAndFlush(NFP).addListener(ChannelFutureListener.CLOSE);
return;
@@ -111,10 +111,10 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
});
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler(
settingService.getSystem(Setting.Module.Music.CONTROLLER_URI).asString(),
settingService.getSystem(MusicSetting.CONTROLLER_URI).asString(),
null,
true,
settingService.getSystem(Setting.Module.Music.MAX_FRAME_LENGTH).asLong()
settingService.getSystem(MusicSetting.MAX_FRAME_LENGTH).asLong()
)
);
@@ -123,7 +123,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
}
});
channel = bootstrap.bind().sync().channel();
log.info("TimiMusicRC controller service startup with {}", settingService.getSystem(Setting.Module.Music.CONTROLLER_IP).asString());
log.info("TimiMusicRC controller service startup with {}", settingService.getSystem(MusicSetting.CONTROLLER_IP).asString());
} catch (Exception e) {
log.error("TimiMusicRC controller service startup error", e);
}
@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.music.runner;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.music.bean.MusicSetting;
import com.imyeyu.api.modules.music.handler.PlayerMessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
@@ -49,7 +49,7 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
}
public void run(ApplicationArguments args) {
if (settingService.getSystem(Setting.Module.Music.ENABLE).isTrue()) {
if (settingService.getSystem(MusicSetting.ENABLE).isTrue()) {
return;
}
ServerBootstrap bootstrap = new ServerBootstrap();
@@ -58,13 +58,13 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
try {
bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(Setting.Module.Music.PLAYER_PORT).asInteger()));
bootstrap.localAddress(new InetSocketAddress(settingService.getSystem(MusicSetting.PLAYER_PORT).asInteger()));
bootstrap.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
settingService.getSystem(Setting.Module.Music.MAX_FRAME_LENGTH).asInteger(),
settingService.getSystem(MusicSetting.MAX_FRAME_LENGTH).asInteger(),
0,
4,
0,
@@ -79,7 +79,7 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
channel = bootstrap.bind().sync().channel();
log.info("TimiMusicRC player service startup with {}", settingService.getSystem(Setting.Module.Music.PLAYER_IP).asString());
log.info("TimiMusicRC player service startup with {}", settingService.getSystem(MusicSetting.PLAYER_IP).asString());
} catch (Exception e) {
log.error("TimiMusicRC player service startup error", e);
}
@@ -61,7 +61,7 @@ public class ServerFile implements Serializable {
name = file.getName();
absolutePath = file.getAbsolutePath().replaceAll("\\\\", "/");
SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class);
absolutePath = absolutePath.substring("E:\\IDEAProject\\TimiServerAPI\\data\\file".length());
absolutePath = absolutePath.substring(new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString()).getAbsolutePath().length());
modifiedAt = file.lastModified();
this.file = file.isFile();
@@ -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;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.api.modules.system.bean.ServerFile;
import com.imyeyu.api.modules.system.bean.SystemFileSetting;
import com.imyeyu.api.modules.system.bean.TransferFile;
import com.imyeyu.api.modules.system.service.AsyncTaskService;
import com.imyeyu.api.modules.system.service.FileService;
@@ -47,7 +46,6 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -67,7 +65,6 @@ import java.util.Map;
@RequestMapping("/system/file")
public class FileController implements TimiJava, OS.FileSystem {
private final TagService tagService;
private final FileService service;
private final SettingService settingService;
private final AsyncTaskService asyncTaskService;
@@ -143,7 +140,7 @@ public class FileController implements TimiJava, OS.FileSystem {
public void read(HttpServletRequest req, HttpServletResponse resp) {
try {
String path = req.getServletPath().substring("/system/file/read".length());
path = "/root/bin/data/mnt" + path;
path = settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path;
if (TimiJava.isEmpty(path)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
@@ -195,8 +192,7 @@ public class FileController implements TimiJava, OS.FileSystem {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// path = settingService.getAsString(Setting.Module.System.FILE_BASE_PATH) + path;
File file = new File(path);
File file = new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path);
if (!file.exists()) {
log.warn("preview a not exist file: {}", path);
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
@@ -409,8 +405,7 @@ public class FileController implements TimiJava, OS.FileSystem {
public void download(HttpServletResponse resp) {
try {
String path = TimiSpring.cutURIStartAt("/system/file/download");
path = "E:/IDEAProject/TimiServerAPI/data/file" + path;
File file = new File(path);
File file = new File(settingService.getSystem(SystemFileSetting.BASE_PATH).asString() + path);
service.checkAccessPermission(file.getAbsolutePath());
String mimeType = new Tika().detect(file);
@@ -1,8 +1,8 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerFile;
import com.imyeyu.api.modules.system.bean.SystemFileSetting;
import com.imyeyu.api.modules.system.bean.TransferFile;
import com.imyeyu.api.modules.system.service.AsyncTaskService;
import com.imyeyu.api.modules.system.service.FileService;
@@ -257,8 +257,7 @@ public class FileServiceImplement implements TimiJava, FileService {
}
private String getPath(String path) {
// String basePath = settingService.getSystem(Setting.Module.System.FILE_BASE_PATH).asString();
String basePath = "E:\\IDEAProject\\TimiServerAPI\\data\\file";
String basePath = settingService.getSystem(SystemFileSetting.BASE_PATH).asString();
if (TimiJava.isEmpty(path)) {
return basePath;
}
@@ -1,8 +1,8 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.bean.SystemStatusSetting;
import com.imyeyu.api.modules.system.service.StatusService;
import com.imyeyu.api.modules.system.vo.SystemStatusDataView;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
@@ -33,7 +33,7 @@ public class StatusServiceImplement implements StatusService {
@Override
public SystemStatusSnapshotView getStatus(String metrics) {
long serverTime = Time.now();
int sampleRateMs = settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger();
int sampleRateMs = settingService.getSystem(SystemStatusSetting.RATE).asInteger();
EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) {
@@ -48,7 +48,7 @@ public class StatusServiceImplement implements StatusService {
@Override
public SystemStatusHistoryView getStatusHistory(String window, String metrics) {
long serverTime = Time.now();
int sampleRateMs = settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger();
int sampleRateMs = settingService.getSystem(SystemStatusSetting.RATE).asInteger();
EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) {
@@ -261,7 +261,7 @@ public class StatusServiceImplement implements StatusService {
*/
private long parseWindowMs(String window, int sampleRateMs) {
if (window == null || window.isBlank()) {
return (long) settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger() * sampleRateMs;
return (long) settingService.getSystem(SystemStatusSetting.RATE).asInteger() * sampleRateMs;
}
String normalized = window.trim().toLowerCase(Locale.ROOT);
@@ -277,7 +277,7 @@ public class StatusServiceImplement implements StatusService {
try {
return Math.max(sampleRateMs, Long.parseLong(valueText) * unit);
} catch (NumberFormatException e) {
return (long) settingService.getSystem(Setting.Module.SystemStatus.RATE).asInteger() * sampleRateMs;
return (long) settingService.getSystem(SystemStatusSetting.RATE).asInteger() * sampleRateMs;
}
}
@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.system.task.status;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.system.bean.SystemStatusSetting;
import java.util.Deque;
@@ -22,7 +22,7 @@ public abstract class AbstractDequeStatusCollector implements StatusCollector {
*/
protected <T> void putDeque(StatusCollectContext context, Deque<T> deque, T value) {
deque.addLast(value);
if (context.getSettingService().getSystem(Setting.Module.SystemStatus.LIMIT).asInteger() < deque.size()) {
if (context.getSettingService().getSystem(SystemStatusSetting.LIMIT).asInteger() < deque.size()) {
deque.pollFirst();
}
}
@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.system.bean.SystemStatusSetting;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import lombok.extern.slf4j.Slf4j;
@@ -24,7 +24,7 @@ public class NetworkStatusCollector extends AbstractDequeStatusCollector {
@Override
public void initialize(StatusCollectContext context) {
List<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) {
if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes();
@@ -41,8 +41,8 @@ public class NetworkStatusCollector extends AbstractDequeStatusCollector {
@Override
public void collect(StatusCollectContext context) {
String targetMac = context.getSettingService().getSystem(Setting.Module.SystemStatus.NETWORK_MAC).asString();
int sampleRateMs = context.getSettingService().getSystem(Setting.Module.SystemStatus.RATE).asInteger();
String targetMac = context.getSettingService().getSystem(SystemStatusSetting.NETWORK_MAC).asString();
int sampleRateMs = context.getSettingService().getSystem(SystemStatusSetting.RATE).asInteger();
for (NetworkIF networkIF : context.getHardware().getNetworkIFs()) {
if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes();
@@ -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.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.SmsService;
import com.imyeyu.api.modules.common.vo.SmsCaptchaVerifyRequest;
import com.imyeyu.api.modules.user.bean.BootstrapData;
import com.imyeyu.api.modules.user.bean.UserSetting;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserRoleService;
@@ -79,7 +79,7 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
@Override
public LoginResponse login(String token, String userId) {
Long time = settingService.getSystem(Setting.Module.User.TOKEN_TTL).asTime();
Long time = settingService.getSystem(UserSetting.TOKEN_TTL).asTime();
long now = Time.now();
long expireAt = now + time;
redisUserToken.set(getUserTokenKey(token), userId, expireAt - now);
@@ -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 同单位重复由既有唯一键阻止
@@ -103,14 +103,14 @@
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END), 0) AS `directQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END), 0) AS `packageQuantity`,
COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`,
COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`
COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`base_quantity` ELSE 0 END), 0) AS `directBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` ELSE 0 END), 0) AS `packageBaseQuantity`,
COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`
FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id`
INNER JOIN `gao_product` p ON p.`id` = i.`product_id_snapshot` AND p.`store_id` = o.`store_id`
@@ -127,9 +127,9 @@
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`,
COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`
COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`
FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id`
INNER JOIN `gao_product` p ON p.`id` = i.`product_id_snapshot` AND p.`store_id` = o.`store_id`
@@ -86,14 +86,14 @@
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END), 0) AS `directQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END), 0) AS `packageQuantity`,
COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`,
COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`,
COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`base_quantity` ELSE 0 END), 0) AS `directBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` ELSE 0 END), 0) AS `packageBaseQuantity`,
COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN COALESCE(i.`line_amount`, 0) ELSE 0 END), 0) AS `directSaleAmount`
FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id`
@@ -124,11 +124,11 @@
i.`product_id_snapshot` AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`,
COALESCE(SUM(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`base_quantity` ELSE 0 END), 0) AS `directBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` ELSE 0 END), 0) AS `packageBaseQuantity`,
COALESCE(SUM(i.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 0) AS `availableBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN COALESCE(i.`line_amount`, 0) ELSE 0 END), 0) AS `directSaleAmount`
FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id`
@@ -20,16 +20,16 @@
WHERE `id` = #{id} AND `deleted_at` IS NULL
FOR UPDATE
</select>
<update id="updateReservedQuantity">
<update id="updateReservedBaseQuantity">
UPDATE `gao_sales_order_item`
SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `reserved_quantity` + #{delta} &gt;= 0
AND `fulfilled_quantity` + `reserved_quantity` + #{delta} &lt;= `quantity`
SET `reserved_base_quantity` = `reserved_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `reserved_base_quantity` + #{delta} &gt;= 0
AND `fulfilled_base_quantity` + `reserved_base_quantity` + #{delta} &lt;= `base_quantity`
</update>
<update id="updateFulfilledQuantity">
<update id="updateFulfilledBaseQuantity">
UPDATE `gao_sales_order_item`
SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `fulfilled_quantity` + #{delta} &gt;= 0
AND `fulfilled_quantity` + #{delta} &lt;= `quantity`
SET `fulfilled_base_quantity` = `fulfilled_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `fulfilled_base_quantity` + #{delta} &gt;= 0
AND `fulfilled_base_quantity` + #{delta} &lt;= `base_quantity`
</update>
</mapper>