Compare commits

..
39 Commits
Author SHA1 Message Date
timi d65424e5bf Merge pull request 'v1.0.43' (#55) from dev into master
Reviewed-on: #55
2026-09-11 17:26:21 +00:00
Timi f0b72b70e7 v1.0.43
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-12 01:24:03 +08:00
Timi daa5a6aa5e fix off product sku for keep fulfillment 2026-09-12 01:23:47 +08:00
timi 2a35793baf Merge pull request 'v1.0.42' (#54) from dev into master
Reviewed-on: #54
2026-09-11 16:16:04 +00:00
Timi 4a157e9578 v1.0.42
CI / build-deploy (pull_request) Successful in 24s
CI / notify-on-failure (pull_request) Skipped
2026-09-12 00:11:21 +08:00
Timi eabb4c6aa6 fix event record CONTINUOUS_DAYS rank 2026-09-12 00:11:07 +08:00
timi d22df698b6 Merge pull request 'v1.0.41' (#53) from dev into master
Reviewed-on: #53
2026-09-11 15:55:02 +00:00
Timi cb6051cc41 v1.0.41
CI / build-deploy (pull_request) Successful in 59s
CI / notify-on-failure (pull_request) Skipped
2026-09-11 23:53:06 +08:00
Timi 1d1009b121 add customer list for GaoCustomerDailyStateView 2026-09-11 23:52:43 +08:00
Timi 94f3fbb867 add customer age and disease chart 2026-09-11 20:24:09 +08:00
timi 1771bf9909 Merge pull request 'v1.0.40' (#52) from dev into master
Reviewed-on: #52
2026-09-10 16:29:28 +00:00
Timi fc4d55517b v1.0.40
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-11 00:26:14 +08:00
Timi ec72a99d74 response customer pinyin 2026-09-11 00:24:58 +08:00
Timi a431db6e6e fix article detail request method 2026-09-10 20:27:53 +08:00
Timi 9cfb31b015 support pinyin for product 2026-09-10 20:27:32 +08:00
timi 15d69f9361 Merge pull request 'v1.0.39' (#51) from dev into master
Reviewed-on: #51
2026-09-10 06:10:30 +00:00
Timi b000390a75 v1.0.39
CI / build-deploy (pull_request) Successful in 24s
CI / notify-on-failure (pull_request) Skipped
2026-09-10 14:09:49 +08:00
Timi 1b9f92caf5 add setting map 2026-09-10 14:09:38 +08:00
Timi 7b08641acc add customer order map 2026-09-10 14:09:30 +08:00
Timi 66c13062b2 fix notify fail 2026-09-10 12:47:29 +08:00
timi 2b4089ef59 Merge pull request 'v1.0.38' (#50) from dev into master
Reviewed-on: #50
2026-09-09 17:33:25 +00:00
Timi cdf8a426e9 v1.0.38
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-10 01:31:20 +08:00
Timi a2d69fe721 support gift 2026-09-10 01:31:06 +08:00
Timi 19db357b44 update article CRUD 2026-09-10 01:30:50 +08:00
Timi 07ffef3e2a add cancel operator 2026-09-09 17:14:29 +08:00
timi 20c3e1f422 Merge pull request 'v1.0.37' (#49) from dev into master
Reviewed-on: #49
2026-09-09 06:21:55 +00:00
Timi f93ec3cd4d v1.0.37
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-09 14:21:18 +08:00
Timi 3cdac98832 add allowDecimalQuantity 2026-09-09 14:21:04 +08:00
Timi c122c35376 refactor LoggerService.excute 2026-09-09 13:04:16 +08:00
timi 3b19d9e930 Merge pull request 'v1.0.36' (#48) from dev into master
Reviewed-on: #48
2026-09-08 17:52:47 +00:00
Timi 2ea06fb5c0 v1.0.36
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-09 01:49:52 +08:00
Timi 00bb300755 support split fulfillment by base unit 2026-09-09 01:49:40 +08:00
Timi ea44dd5c0e update point rule groovy script 2026-09-09 01:08:45 +08:00
Timi 818207d6c9 fix FileController 2026-09-08 18:09:31 +08:00
Timi af76eb43b8 update SystemSetting 2026-09-08 16:52:32 +08:00
timi 5fd11e9caf Merge pull request 'v1.0.35' (#47) from dev into master
Reviewed-on: #47
2026-09-07 17:06:28 +00:00
Timi d920b8630d v1.0.35
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-08 01:05:45 +08:00
timi 0b98dcdfe9 Merge pull request 'v1.0.34' (#46) from dev into master
Reviewed-on: #46
2026-09-07 15:11:06 +00:00
Timi 1e62576d36 v1.0.34
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-07 23:10:27 +08:00
122 changed files with 2247 additions and 1109 deletions
+6 -1
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId>
<version>1.0.33</version>
<version>1.0.43</version>
<packaging>jar</packaging>
<name>TimiServerAPI</name>
<description>imyeyu.com API</description>
@@ -215,6 +215,11 @@
<artifactId>tinypinyin</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.4</version>
</dependency>
<dependency>
<groupId>com.imyeyu.font.icon</groupId>
<artifactId>timi-icon-font</artifactId>
@@ -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();
}
@@ -45,6 +45,11 @@ public enum CorePermissionCode implements BuiltinPermissionCode {
TAG_UPDATE("TAG:UPDATE", "标签修改", false, true),
TAG_DELETE("TAG:DELETE", "标签删除", false, true),
ARTICLE_CREATE("ARTICLE:CREATE", "文章创建", false, true),
ARTICLE_READ("ARTICLE:READ", "文章读取", false, true),
ARTICLE_UPDATE("ARTICLE:UPDATE", "文章修改", false, true),
ARTICLE_DELETE("ARTICLE:DELETE", "文章删除", false, true),
USER_CREATE("USER:CREATE", "用户创建", false, true),
USER_READ("USER:READ", "用户读取", false, true),
USER_UPDATE("USER:UPDATE", "用户修改", false, true),
@@ -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();
}
@@ -32,4 +32,22 @@ public class ThreadPoolConfig {
executor.initialize();
return executor;
}
/// 创建操作审计线程池
///
/// @return 操作审计线程池
@Bean(name = "auditTaskExecutor")
public ThreadPoolTaskExecutor auditTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(2000);
executor.setKeepAliveSeconds(60);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.setThreadNamePrefix("audit-task-executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@@ -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;
}
@@ -0,0 +1,96 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.ArticleService;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/// 文章接口
///
/// @author Codex
/// @since 2026-09-09
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/article")
public class ArticleController {
private final ArticleService service;
private final AttachmentService attachmentService;
/// 查询文章列表
///
/// @param page 分页参数
/// @return 文章分页
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.ARTICLE_READ)
@PostMapping("/list")
public PageResult<Article> list(@RequestBody Page<Article> page) {
return service.page(page);
}
/// 查询文章详情
///
/// @param id 文章 ID
/// @return 文章详情
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@RequestMapping("/detail")
public Article detail(@RequestParam String id) {
Article article = service.get(id);
article.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId()));
return article;
}
/// 创建文章
///
/// @param article 文章
/// @return 创建后的文章
@AOPLog
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.ARTICLE_CREATE)
@PostMapping("/create")
public Article create(@RequestBody Article article) {
service.create(article);
return article;
}
/// 更新文章
///
/// @param article 文章
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.ARTICLE_UPDATE)
@PostMapping("/update")
public void update(@RequestBody Article article) {
service.update(article);
}
/// 删除文章
///
/// @param id 文章 ID
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.ARTICLE_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
}
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.common.vo.ClientLogRequest;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.spring.annotation.AOPLog;
@@ -29,7 +30,9 @@ public class LoggerController {
@AOPLog
@RequiredToken
@PostMapping("/create")
public void create(@Valid @RequestBody Logger logger) {
public void create(@Valid @RequestBody ClientLogRequest request) {
Logger logger = Logger.info(Logger.Module.COMMON, "CLIENT_LOG");
logger.setContent(request.getContent());
service.create(logger);
}
}
@@ -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;
}
@@ -15,8 +15,12 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
*
*
@@ -26,11 +30,30 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/setting")
@RequestMapping("/setting")
public class SettingController {
private final SettingService service;
/**
* 查询公开配置映射
*
* @param bizType 业务类型
* @param bizId 业务 ID,可为空
* @param code 配置代码列表,可为空
* @return 公开配置映射
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@RequestMapping("/map")
public Map<String, Setting> map(
@RequestParam String bizType,
@RequestParam(required = false) String bizId,
@RequestParam(required = false) List<String> code
) {
return service.mapPublic(bizType, bizId, code);
}
/**
* 查询配置列表
*
@@ -53,6 +76,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 +90,7 @@ public class SettingController {
*/
@AOPLog
@JsonView(ResponseView.Admin.class)
@RequireCorePermission(CorePermissionCode.SETTING_CREATE)
@RequestRateLimit
@PostMapping("/create")
public Setting create(@RequestBody Setting setting) {
@@ -80,6 +105,7 @@ public class SettingController {
*/
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_UPDATE)
@PostMapping("/update")
public void update(@RequestBody Setting setting) {
service.update(setting);
@@ -34,6 +34,8 @@ public class Article extends UUIDEntity implements SettingSupport {
/// GAO 门店日记
GAO_STORE_DIARY,
GAO_SYSTEM,
/// 反馈
FEEDBACK
}
@@ -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;
}
@@ -20,11 +20,11 @@ public class Notify extends UUIDEntity {
/** 业务类型 */
private String bizType;
/** 提醒业务类型 */
/** 业务提醒类型 */
private String bizRemindedType;
/** 提醒业务 ID */
private String bizRemindedId;
/** 业务提醒查重键 */
private String bizRemindedKey;
/** 参数 */
private String args;
@@ -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);
}
@@ -2,9 +2,8 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -16,9 +15,8 @@ import java.util.List;
*/
public interface NotifyMapper extends BaseMapper<Notify, String> {
/// 查询业务提醒是否已创建
@Select("SELECT EXISTS (SELECT 1 FROM `notify` WHERE `biz_type` = #{bizType} AND `biz_reminded_type` = #{bizRemindedType} AND `biz_reminded_id` = #{bizRemindedId} AND `deleted_at` IS NULL)")
boolean existsByBizReminded(@Param("bizType") String bizType, @Param("bizRemindedType") String bizRemindedType, @Param("bizRemindedId") String bizRemindedId);
/// 原子创建业务提醒
int insertIfAbsent(Notify notify);
/// 分页查询管理端通知
///
@@ -3,6 +3,8 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.spring.mapper.BaseMapper;
import java.util.List;
/**
* 系统配置
*
@@ -10,4 +12,6 @@ import com.imyeyu.spring.mapper.BaseMapper;
* @since 2021-07-20 22:26
*/
public interface SettingMapper extends BaseMapper<Setting, String> {
List<Setting> selectPublicByQuery(String bizType, String bizId, List<String> codeList);
}
@@ -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 业务归属
@@ -14,13 +14,10 @@ import com.imyeyu.spring.service.BaseService;
*/
public interface NotifyService extends BaseService<Notify, String> {
/// 查询业务提醒是否已创建
/// 原子创建业务提醒,重复查重键不会创建通知详情
///
/// @param bizType 业务类型
/// @param bizRemindedType 被提醒业务类型
/// @param bizRemindedId 被提醒业务 ID
/// @return true 为已创建
boolean existsByBizReminded(String bizType, String bizRemindedType, String bizRemindedId);
/// @param notify 通知
void createIfAbsent(Notify notify);
/// 分页查询当前用户创建的通知
///
@@ -1,12 +1,13 @@
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;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
import java.util.Map;
/**
* 配置服务
@@ -18,12 +19,22 @@ 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);
/**
* 查询公开配置映射
*
* @param bizType 业务类型
* @param bizId 业务 ID,可为空
* @param code 配置代码列表,可为空
* @return 公开配置映射
*/
Map<String, Setting> mapPublic(String bizType, String bizId, List<String> code);
default List<Setting> listBySettingSupport(SettingSupport settingSupport) {
String bizType = settingSupport.getClass().getSimpleName().toUpperCase();
return listByBizId(bizType, settingSupport.getId());
@@ -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,10 +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.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.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
@@ -17,12 +16,13 @@ import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 通知服务实现
@@ -30,13 +30,10 @@ import java.util.List;
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class NotifyServiceImplement extends AbstractEntityService<Notify, String> implements NotifyService {
private final LoggerService loggerService;
private final UserLoginService userLoginService;
private final NotifyDetailService detailService;
private final NotifyMapper mapper;
@@ -46,45 +43,53 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
return mapper;
}
@Override
public boolean existsByBizReminded(String bizType, String bizRemindedType, String bizRemindedId) {
return mapper.existsByBizReminded(bizType, bizRemindedType, bizRemindedId);
}
/// 创建通知并级联创建通知详情
///
/// @param notify 通知
@OperationAudit(AuditOperation.COMMON_NOTIFY_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(Notify notify) {
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_NOTIFY_CREATE");
try {
TimiException.required(notify, "未找到通知");
TimiException.required(notify.getDetailList(), "未找到通知详情列表");
String sendFrom = userLoginService.isLogged() ? userLoginService.getRequireLoginUserId() : null;
notify.setCreatedBy(sendFrom);
super.create(notify);
prepareCreate(notify);
super.create(notify);
createDetails(notify);
}
for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId());
if (TimiJava.isNotEmpty(sendFrom)) {
detail.setSendFrom(sendFrom);
}
detailService.create(detail);
}
logger.setLevel(Logger.Level.INFO);
logger.setResult(notify.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("创建通知失败", e);
throw new TimiException(TimiCode.ERROR, "创建通知失败", e);
} finally {
loggerService.create(logger);
/// 原子创建业务提醒并级联创建通知详情
///
/// @param notify 通知
@OperationAudit(AuditOperation.COMMON_NOTIFY_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void createIfAbsent(Notify notify) {
prepareCreate(notify);
if (mapper.insertIfAbsent(notify) == 0) {
return;
}
createDetails(notify);
}
/// 校验并补全通知创建字段
///
/// @param notify 通知
private void prepareCreate(Notify notify) {
TimiException.required(notify, "未找到通知");
TimiException.required(notify.getDetailList(), "未找到通知详情列表");
notify.setId(TimiJava.defaultIfEmpty(notify.getId(), UUID.randomUUID().toString()));
notify.setCreatedBy(TimiJava.defaultIfEmpty(notify.getCreatedBy(), TimiJava.ZERO_UUID));
notify.setCreatedAt(TimiJava.defaultIfNull(notify.getCreatedAt(), Time.now()));
notify.setUpdatedAt(null);
notify.setDeletedAt(null);
}
/// 创建通知详情
///
/// @param notify 通知
private void createDetails(Notify notify) {
for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId());
detail.setSendFrom(TimiJava.defaultIfEmpty(detail.getSendFrom(), TimiJava.ZERO_UUID));
detailService.create(detail);
}
}
@@ -122,6 +127,8 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
/// @param notifyId 通知 ID
/// @param userId 用户 ID
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
@OperationAudit(AuditOperation.COMMON_NOTIFY_DELETE)
public void deleteManagement(String notifyId, String userId) {
getManagement(notifyId, userId);
delete(notifyId);
@@ -132,6 +139,7 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
/// @param notifyId 通知 ID
/// @param userId 用户 ID
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public void cancelManagement(String notifyId, String userId) {
getManagement(notifyId, userId);
cancel(notifyId);
@@ -140,32 +148,17 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
/// 删除通知并级联软删除通知详情
///
/// @param notifyId 通知 ID
@OperationAudit(AuditOperation.COMMON_NOTIFY_DELETE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String notifyId) {
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_NOTIFY_DELETE");
try {
TimiException.required(notifyId, "未找到通知 ID");
TimiException.required(get(notifyId), "未找到通知");
List<NotifyDetail> detailList = detailService.listByNotifyId(notifyId);
for (NotifyDetail detail : detailList) {
detailService.delete(detail.getId());
}
super.delete(notifyId);
logger.setLevel(Logger.Level.INFO);
logger.setResult(notifyId);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("删除通知失败", e);
throw new TimiException(TimiCode.ERROR, "删除通知失败", e);
} finally {
loggerService.create(logger);
TimiException.required(notifyId, "未找到通知 ID");
TimiException.required(get(notifyId), "未找到通知");
List<NotifyDetail> detailList = detailService.listByNotifyId(notifyId);
for (NotifyDetail detail : detailList) {
detailService.delete(detail.getId());
}
super.delete(notifyId);
}
/// 取消通知中尚未成功发送的通知详情
@@ -17,6 +17,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 系统配置服务实现
@@ -53,6 +55,12 @@ public class SettingServiceImplement extends AbstractEntityService<Setting, Stri
return mapper.selectAllByExample(example);
}
@Override
public Map<String, Setting> mapPublic(String bizType, String bizId, List<String> code) {
TimiException.required(bizType, "not found bizType");
return mapper.selectPublicByQuery(bizType, bizId, code).stream().collect(Collectors.toMap(Setting::getCode, Function.identity()));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void updateByBizId(String bizType, String bizId, List<Setting> list) {
@@ -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);
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.common.vo;
import jakarta.validation.constraints.Size;
import lombok.Data;
/// 客户端诊断日志请求
///
/// 客户端日志不是操作审计,操作编码、模块、级别、用户和 IP 均由服务端决定
///
/// @author Codex
/// @since 2026-09-09
@Data
public class ClientLogRequest {
/// 日志内容
@Size(max = 32768, message = "日志内容不能超过 32 KB")
private String content;
}
@@ -111,6 +111,8 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
CUSTOMER_PACKAGE_PRICE_OVERRIDE("CUSTOMER_PACKAGE:PRICE_OVERRIDE", "客户套餐成交价和权益配额调整", true, true),
CUSTOMER_PACKAGE_GIFT_CREATE("CUSTOMER_PACKAGE:GIFT_CREATE", "客户套餐创建赠送", true, true),
FUND_RECORD_READ("FUND_RECORD:READ", "资金流水读取", true, true),
FUND_RECEIVE_CREATE("FUND_RECEIVE:CREATE", "线下收款创建", true, true),
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.gao.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-09-09 16:11
@Getter
@SettingBizType("GAO_SYSTEM")
@AllArgsConstructor
public enum GaoSystemSetting implements SettingEnum {
SERVICE_LICENSE(null, Setting.ValueType.STRING),
PRIVACY_LICENSE(null, Setting.ValueType.STRING),
OPEN_SOURCE_LICENSE(null, Setting.ValueType.STRING);
final String defaultValue;
final Setting.ValueType valueType;
}
@@ -5,6 +5,8 @@ import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
@@ -51,4 +53,15 @@ public class GaoCustomerChartController {
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordCalendar(req);
}
/// 查询客户病症词频
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/disease/word")
public List<GaoCustomerDiseaseWordView> customerDiseaseWord(@RequestBody GaoCustomerDiseaseWordReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.customerDiseaseWord(req);
}
}
@@ -1,8 +1,12 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPageResult;
@@ -15,6 +19,9 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/// GAO 客户消费事实接口
///
/// @author Codex
@@ -24,8 +31,10 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/gao/customer/consumption-record")
public class GaoCustomerConsumptionRecordController {
private final GaoCustomerConsumptionRecordService service;
private final AttachmentService attachmentService;
private final GaoCustomerService customerService;
private final GaoStoreScopeService storeScopeService;
private final GaoCustomerConsumptionRecordService service;
@AOPLog
@RequestRateLimit
@@ -34,6 +43,24 @@ public class GaoCustomerConsumptionRecordController {
public GaoCustomerConsumptionRecordPageResult list(@RequestBody GaoCustomerConsumptionRecordPage page) {
TimiException.required(page, "not found consumption record page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return service.pageByQuery(page);
GaoCustomerConsumptionRecordPageResult result = service.pageByQuery(page);
List<String> customerIdList = result.getList()
.stream()
.map(GaoCustomerConsumptionRecord::getCustomerId)
.filter(customerId -> customerId != null && !customerId.isBlank())
.distinct()
.toList();
var customerMap = customerService.mapByIdList(customerIdList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList);
for (GaoCustomerConsumptionRecord record : result.getList()) {
var customer = customerMap.get(record.getCustomerId());
if (customer == null) {
continue;
}
customer.setAttachmentList(attachmentMap.getOrDefault(customer.getId(), List.of()));
record.setCustomer(customer);
}
return result;
}
}
@@ -12,6 +12,7 @@ import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
@@ -42,6 +43,7 @@ public class GaoFundRecordController {
private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService;
private final UserService userService;
private final UserLoginService userLoginService;
@AOPLog
@RequestRateLimit
@@ -66,6 +68,33 @@ public class GaoFundRecordController {
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/my-receive/list")
public PageResult<GaoFundRecord> listMyReceive(@RequestBody GaoFundRecordPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
page.setOperatorUserId(userLoginService.getRequireLoginUserId());
page.setOperationType(GaoFundRecord.OperationType.RECEIVE);
PageResult<GaoFundRecord> result = service.pageByQuery(page);
fillTransient(result.getList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/my-receive/detail")
public GaoFundRecord detailMyReceive(@RequestParam String id) {
GaoFundRecord result = service.detail(id);
TimiException.requiredTrue(result.getOperationType() == GaoFundRecord.OperationType.RECEIVE, "not found fund receive record");
TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(result.getOperatorUserId()), "只能读取自己提交的收款");
storeScopeService.checkStoreId(result.getStoreId());
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REVOKE)
@@ -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)
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
@@ -16,6 +17,8 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderUpdateRequest;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
@@ -39,6 +42,7 @@ public class GaoSalesOrderController {
private final GaoSalesOrderService service;
private final GaoOrderPaymentService paymentService;
private final GaoStoreScopeService storeScopeService;
private final UserService userService;
@AOPLog
@RequestRateLimit
@@ -55,7 +59,11 @@ public class GaoSalesOrderController {
@RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ)
@PostMapping("/detail")
public GaoSalesOrder detail(@RequestParam String id) {
return service.detail(id);
GaoSalesOrder result = service.detail(id);
if (TimiJava.isNotEmpty(result.getCancelOperatorUserId())) {
result.setCancelOperator(userService.get(result.getCancelOperatorUserId()));
}
return result;
}
@AOPLog
@@ -112,7 +120,7 @@ public class GaoSalesOrderController {
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@RequireGaoPermission(value = {GaoPermissionCode.FUND_RECEIVE_CREATE, GaoPermissionCode.FUND_RECORD_READ}, mode = PermissionMatchMode.ANY)
@PostMapping("/payment/cancel")
public GaoFundRecord cancelPayment(@RequestBody GaoOrderPaymentActionRequest request) {
return paymentService.cancel(request);
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
@@ -54,6 +55,15 @@ public class GaoStoreChartController {
return service.customerGender(storeScopeService.resolveStoreId(storeId));
}
/// 查询客户年龄分布
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/age")
public List<GaoCustomerAgeStatView> customerAge(@RequestParam String storeId) {
return service.customerAge(storeScopeService.resolveStoreId(storeId));
}
/// 查询登记事件每日统计,可同时支撑登记趋势和事件结构图表
@AOPLog
@RequestRateLimit
@@ -6,7 +6,6 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.api.modules.user.bean.Gender;
import com.imyeyu.api.modules.user.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.annotation.table.Transient;
@@ -62,15 +61,12 @@ public class GaoCustomer extends UUIDEntity {
private String name;
@Column("name_pinyin_full")
@JsonIgnore
private String namePinyinFull;
@Column("name_pinyin_initial")
@JsonIgnore
private String namePinyinInitial;
@Column("name_pinyin_mixed")
@JsonIgnore
private String namePinyinMixed;
/// 性别
@@ -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;
}
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.gao.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
@@ -61,6 +63,18 @@ public class GaoProduct extends UUIDEntity {
/// 产品名称
private String name;
@Column("name_pinyin_full")
@JsonIgnore
private String namePinyinFull;
@Column("name_pinyin_initial")
@JsonIgnore
private String namePinyinInitial;
@Column("name_pinyin_mixed")
@JsonIgnore
private String namePinyinMixed;
/// 产品类型
private ProductType productType;
@@ -34,6 +34,9 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 订单项类型
private ItemType itemType;
/// true 为赠送权益
private Boolean gifted;
/// SKU ID
private String skuId;
@@ -68,14 +71,25 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 基础单位数量系数快照
private BigDecimal baseQuantityPerUnitSnapshot;
/// 当前 SKU 是否允许业务数量保留小数
@Transient
private Boolean allowDecimalQuantity;
/// 当前 SKU 基础单位名称
@Transient
private String baseUnitName;
/// 购买数量
private BigDecimal quantity;
/// 待履约数量
private BigDecimal reservedQuantity;
/// 购买基础单位数量
private BigDecimal baseQuantity;
/// 已履约数量
private BigDecimal fulfilledQuantity;
/// 待履约基础单位数量
private BigDecimal reservedBaseQuantity;
/// 已履约基础单位数量
private BigDecimal fulfilledBaseQuantity;
/// 实际单价,单位分
private Long unitPrice;
@@ -89,15 +103,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;
}
@@ -1,7 +1,10 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateCustomerView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseTextView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
import com.imyeyu.spring.mapper.BaseMapper;
@@ -27,6 +30,14 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @return 每日新增客户数
List<GaoCustomerDailyStateView> stateDailyNewCustomer(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt);
/// 查询每日新增客户摘要
///
/// @param storeId 门店 ID
/// @param beginCreatedAt 开始创建时间
/// @param endCreatedAt 结束创建时间
/// @return 每日新增客户摘要
List<GaoCustomerDailyStateCustomerView> selectDailyNewCustomer(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt);
/// 统计开始时间前客户总数
///
/// @param beginCreatedAt 开始创建时间
@@ -38,6 +49,18 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @return 性别统计列表
List<GaoCustomerGenderStatView> stateGender(@Param("storeId") String storeId);
/// 统计客户年龄分布
///
/// @param storeId 门店 ID
/// @return 客户年龄列表
List<GaoCustomerAgeStatView> stateAge(@Param("storeId") String storeId);
/// 查询客户病症原始文本
///
/// @param storeId 门店 ID
/// @return 客户备注和病症
List<GaoCustomerDiseaseTextView> selectDiseaseTextByStoreId(@Param("storeId") String storeId);
/// 查询客户总数
///
/// @param page 查询条件
@@ -55,5 +78,5 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @param customerId 客户 ID
/// @param delta 变化量
@Update("UPDATE `gao_customer` SET `invited_count` = GREATEST(IFNULL(`invited_count`, 0) + #{delta}, 0), `updated_at` = " + UNIX_TIME + " WHERE `id` = #{customerId}" + NOT_DELETE)
void updateInvitedCountDelta(@Param("customerId") String customerId, @Param("delta") Long delta);
void updateInvitedCountDelta(String customerId, Long delta);
}
@@ -13,6 +13,12 @@ import java.util.List;
/// @since 2026-09-06
public interface GaoProductSkuMapper extends BaseMapper<GaoProductSku, String> {
/// 批量查询 SKU
///
/// @param idList SKU ID 集合
/// @return SKU 列表
List<GaoProductSku> selectByIdList(@Param("idList") Collection<String> idList);
/// 按产品查询 SKU
List<GaoProductSku> selectByProductId(@Param("productId") String productId);
@@ -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);
}
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
@@ -17,4 +19,7 @@ public interface GaoCustomerChartService {
/// 查询客户登记日历记录
List<GaoEventRecordCalendarView> eventRecordCalendar(GaoCustomerChartReq req);
/// 查询客户病症词频
List<GaoCustomerDiseaseWordView> customerDiseaseWord(GaoCustomerDiseaseWordReq req);
}
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
@@ -87,4 +88,10 @@ public interface GaoCustomerService extends BaseService<GaoCustomer, String> {
/// @param storeId 门店 ID
/// @return 性别统计列表
List<GaoCustomerGenderStatView> stateGender(String storeId);
/// 统计客户年龄分布
///
/// @param storeId 门店 ID
/// @return 客户年龄列表
List<GaoCustomerAgeStatView> stateAge(String storeId);
}
@@ -22,7 +22,7 @@ public interface GaoOrderPaymentService {
/// 拒绝收款
GaoFundRecord reject(GaoOrderPaymentActionRequest request);
/// 撤销自己提交的收款
/// 撤销待核销收款
GaoFundRecord cancel(GaoOrderPaymentActionRequest request);
/// 登记退款
@@ -15,6 +15,12 @@ import java.util.List;
/// @since 2026-09-06
public interface GaoProductSkuService extends BaseService<GaoProductSku, String> {
/// 批量查询 SKU
///
/// @param idList SKU ID 集合
/// @return SKU 列表,包含单位、规格和附件临时数据
List<GaoProductSku> listByIdList(Collection<String> idList);
/// 查询 SKU 详情
GaoProductSku detail(String id);
@@ -27,6 +33,14 @@ public interface GaoProductSkuService extends BaseService<GaoProductSku, String>
/// 查询并校验可销售 SKU 单位
GaoProductSkuUnitSelection getSellableSkuUnit(String storeId, String skuId, String skuUnitId);
/// 查询并校验可用于历史订单履约的 SKU 单位
///
/// @param storeId 门店 ID
/// @param skuId SKU ID
/// @param skuUnitId SKU 单位 ID
/// @return SKU 单位选择结果
GaoProductSkuUnitSelection getFulfillableSkuUnit(String storeId, String skuId, String skuUnitId);
/// 查询并校验 SKU 单位
GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase);
@@ -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);
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
@@ -21,6 +22,9 @@ public interface GaoStoreChartService {
/// 查询客户性别统计
List<GaoCustomerGenderStatView> customerGender(String storeId);
/// 查询客户年龄分布
List<GaoCustomerAgeStatView> customerAge(String storeId);
/// 查询登记事件每日统计
List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req);
@@ -1,15 +1,23 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.hankcs.hanlp.seg.common.Term;
import com.hankcs.hanlp.tokenizer.StandardTokenizer;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseTextView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/// 客户报表服务实现
///
@@ -20,6 +28,7 @@ import java.util.List;
public class GaoCustomerChartServiceImplement implements GaoCustomerChartService {
private final GaoEventRecordService eventRecordService;
private final GaoCustomerMapper customerMapper;
@Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoCustomerChartReq req) {
@@ -33,6 +42,42 @@ public class GaoCustomerChartServiceImplement implements GaoCustomerChartService
return eventRecordService.calendarByCustomerRange(req);
}
@Override
public List<GaoCustomerDiseaseWordView> customerDiseaseWord(GaoCustomerDiseaseWordReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getStoreId(), "not found req.storeId");
int topN = normalizeTopN(req.getTopN());
Map<String, Long> wordCountMap = new HashMap<>();
for (GaoCustomerDiseaseTextView customer : customerMapper.selectDiseaseTextByStoreId(req.getStoreId())) {
countNounWord(customer.getDisease(), wordCountMap);
countNounWord(customer.getRemark(), wordCountMap);
}
return wordCountMap.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey()))
.limit(topN)
.map(item -> new GaoCustomerDiseaseWordView(item.getKey(), item.getValue()))
.toList();
}
private void countNounWord(String text, Map<String, Long> wordCountMap) {
if (text == null || text.isBlank()) {
return;
}
for (Term term : StandardTokenizer.segment(text)) {
if (term.nature != null && term.nature.toString().startsWith("n")) {
wordCountMap.merge(term.word, 1L, Long::sum);
}
}
}
private int normalizeTopN(Integer topN) {
if (topN == null) {
return 60;
}
return Math.clamp(topN, 1, 100);
}
private void checkRangeReq(GaoCustomerChartReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getStoreId(), "not found req.storeId");
@@ -1,11 +1,12 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerConsumptionRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerConsumptionRecordPageResult;
import com.imyeyu.java.bean.timi.TimiException;
@@ -28,6 +29,7 @@ public class GaoCustomerConsumptionRecordServiceImplement extends AbstractEntity
private final GaoCustomerConsumptionRecordMapper mapper;
private final GaoCustomerService customerService;
private final AttachmentService attachmentService;
@Override
protected BaseMapper<GaoCustomerConsumptionRecord, String> mapper() {
@@ -40,12 +42,7 @@ public class GaoCustomerConsumptionRecordServiceImplement extends AbstractEntity
GaoCustomerConsumptionRecordPageResult result = new GaoCustomerConsumptionRecordPageResult();
result.setTotal(mapper.countByQuery(page));
result.setAmountTotal(mapper.sumAmountByPage(page));
List<GaoCustomerConsumptionRecord> list = mapper.selectByQuery(page);
result.setList(list);
var customerMap = customerService.mapByIdList(list.stream().map(GaoCustomerConsumptionRecord::getCustomerId).toList());
for (GaoCustomerConsumptionRecord record : list) {
record.setCustomer(customerMap.get(record.getCustomerId()));
}
result.setList(mapper.selectByQuery(page));
return result;
}
@@ -4,6 +4,7 @@ import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
@@ -22,9 +23,10 @@ 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;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
@@ -38,7 +40,12 @@ 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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -53,6 +60,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;
@@ -62,7 +71,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
private final AttachmentService attachmentService;
private final UserLoginService userLoginService;
private final PermissionChecker permissionChecker;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoCustomerPackage, String> mapper() {
@@ -121,10 +129,11 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
return super.get(id);
}
@OperationAudit(AuditOperation.GAO_CUSTOMER_PACKAGE_PURCHASE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoCustomerPackage purchase(GaoCustomerPackagePurchaseRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_PURCHASE", request, () -> purchaseInternal(request));
return purchaseInternal(request);
}
private GaoCustomerPackage purchaseInternal(GaoCustomerPackagePurchaseRequest request) {
@@ -143,9 +152,16 @@ 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());
normalizePurchaseItems(requestItemList);
verifyPurchaseItems(requestItemList);
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO,
GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
boolean canCreateGift = permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.CUSTOMER_PACKAGE_GIFT_CREATE.getValue());
List<GaoCustomerPackagePurchaseItem> itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList,
canOverridePriceAndQuantity, canCreateGift);
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 +170,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();
@@ -200,10 +211,11 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
return result;
}
@OperationAudit(AuditOperation.GAO_CUSTOMER_PACKAGE_CANCEL)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoCustomerPackage cancel(GaoCustomerPackageCancelRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_CANCEL", request, () -> cancelInternal(request));
return cancelInternal(request);
}
private GaoCustomerPackage cancelInternal(GaoCustomerPackageCancelRequest request) {
@@ -239,6 +251,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
item.setSkuId(configItem.getSkuId());
item.setSkuUnitId(configItem.getSkuUnitId());
item.setGifted(false);
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
item.setProductIdSnapshot(configItem.getProductIdSnapshot());
item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot());
@@ -258,6 +271,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
item.setItemType(GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT);
item.setSkuId(source.getSkuId());
item.setSkuUnitId(source.getSkuUnitId());
item.setGifted(Boolean.TRUE.equals(source.getGifted()));
item.setProductNameSnapshot(source.getProductNameSnapshot());
item.setProductIdSnapshot(source.getProductIdSnapshot());
item.setSkuCodeSnapshot(source.getSkuCodeSnapshot());
@@ -267,8 +281,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());
@@ -276,8 +291,33 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
return item;
}
private void normalizePurchaseItems(List<GaoCustomerPackagePurchaseItem> itemList) {
Map<String, Integer> indexMap = new LinkedHashMap<>();
for (int i = 0; i < itemList.size(); i++) {
GaoCustomerPackagePurchaseItem item = itemList.get(i);
if (item == null) {
continue;
}
item.setGifted(Boolean.TRUE.equals(item.getGifted()));
if (item.getSkuId() == null || item.getSkuUnitId() == null || item.getTotalQuantity() == null) {
continue;
}
String key = item.getSkuId() + "|" + item.getSkuUnitId() + "|" + item.getGifted();
Integer existingIndex = indexMap.putIfAbsent(key, i);
if (existingIndex == null) {
continue;
}
GaoCustomerPackagePurchaseItem existingItem = itemList.get(existingIndex);
if (existingItem == null || existingItem.getTotalQuantity() == null) {
continue;
}
existingItem.setTotalQuantity(existingItem.getTotalQuantity().add(item.getTotalQuantity()));
itemList.set(i, null);
}
itemList.removeIf(item -> item == null);
}
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,46 +325,93 @@ 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();
item.setGifted(Boolean.TRUE.equals(item.getGifted()));
String key = item.getSkuId() + "|" + item.getSkuUnitId() + "|" + item.getGifted();
TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
}
}
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, boolean canCreateGift) {
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) {
boolean gifted = Boolean.TRUE.equals(requestItem.getGifted());
List<GaoPackageConfigItem> skuConfigItemList = configItemMap.get(requestItem.getSkuId());
if (gifted) {
TimiException.requiredTrue(canCreateGift, "没有客户套餐创建赠送权限");
} else {
TimiException.requiredTrue(skuConfigItemList != null && !skuConfigItemList.isEmpty(), "购买 SKU 不属于套餐配置");
}
GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, requestItem.getSkuId(), requestItem.getSkuUnitId());
if (!gifted) {
TimiException.requiredTrue(skuConfigItemList.stream().anyMatch(configItem -> 0 <= configItem.getBaseQuantityPerUnitSnapshot()
.compareTo(selection.getUnit().getBaseQuantityPerUnit())), "套餐权益单位不能高于配置单位");
}
BigDecimal requestBaseQuantity = toBaseQuantity(requestItem.getTotalQuantity(), selection.getUnit().getBaseQuantityPerUnit(), "套餐权益基础数量最多保留三位小数");
if (!gifted) {
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 不允许小数数量");
}
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
item.setSkuId(selection.getSku().getId());
item.setSkuUnitId(selection.getUnit().getId());
item.setGifted(gifted);
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());
if (gifted) {
item.setSort(requestItem.getSort());
item.setRemark(requestItem.getRemark());
} else {
GaoPackageConfigItem template = resolveItemTemplate(skuConfigItemList, selection.getUnit().getId());
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);
}
}
@@ -340,9 +427,10 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
List<GaoCustomerPackagePurchaseItem> requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of());
verifyPurchaseItems(requestItemList);
TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致");
Map<String, GaoSalesOrderItem> existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
Map<String, GaoSalesOrderItem> existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId() + "|"
+ Boolean.TRUE.equals(item.getGifted()), Function.identity()));
for (GaoCustomerPackagePurchaseItem item : requestItemList) {
GaoSalesOrderItem existingItem = existingItemMap.get(item.getSkuId() + "|" + item.getSkuUnitId());
GaoSalesOrderItem existingItem = existingItemMap.get(item.getSkuId() + "|" + item.getSkuUnitId() + "|" + Boolean.TRUE.equals(item.getGifted()));
TimiException.required(existingItem, "requestNo 对应购买请求内容不一致");
TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT
&& existingItem.getQuantity().compareTo(item.getTotalQuantity()) == 0
@@ -14,7 +14,9 @@ import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.util.CustomerXSSFBuilder;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateCustomerView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
@@ -410,6 +412,10 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
for (GaoCustomerDailyStateView view : mapper.stateDailyNewCustomer(req.getStoreId(), beginCreatedAt, endCreatedAt)) {
newCustomerCountMap.put(view.getDateValue(), TimiJava.defaultIfNull(view.getNewCustomerCount(), 0L));
}
Map<Integer, List<GaoCustomerDailyStateCustomerView>> customerListMap = new HashMap<>();
for (GaoCustomerDailyStateCustomerView customer : mapper.selectDailyNewCustomer(req.getStoreId(), beginCreatedAt, endCreatedAt)) {
customerListMap.computeIfAbsent(customer.getDateValue(), key -> new ArrayList<>()).add(customer);
}
long totalCustomerCount = TimiJava.defaultIfNull(mapper.countCreatedBefore(req.getStoreId(), beginCreatedAt), 0L);
List<GaoCustomerDailyStateView> list = new ArrayList<>();
for (LocalDate date = beginDate; !date.isAfter(endDate); date = date.plusDays(1)) {
@@ -420,6 +426,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
view.setDateValue(dateValue);
view.setNewCustomerCount(newCustomerCount);
view.setTotalCustomerCount(totalCustomerCount);
view.setCustomerList(customerListMap.getOrDefault(dateValue, List.of()));
list.add(view);
}
return list;
@@ -431,6 +438,12 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
return mapper.stateGender(storeId);
}
@Override
public List<GaoCustomerAgeStatView> stateAge(String storeId) {
TimiException.required(storeId, "not found storeId");
return mapper.stateAge(storeId);
}
private void changeInvitedCount(String customerId, long delta) {
if (TimiJava.isEmpty(customerId) || delta == 0) {
return;
@@ -29,9 +29,10 @@ 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;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
@@ -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;
@@ -77,7 +79,6 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private final RoleChecker roleChecker;
private final UserLoginService userLoginService;
private final AttachmentService attachmentService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoOrderFulfillment, String> mapper() {
@@ -145,10 +146,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return mapper.countPendingBySalesOrderId(salesOrderId);
}
@OperationAudit(AuditOperation.GAO_ORDER_FULFILLMENT_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CREATE", request, () -> createInternal(request));
return createInternal(request);
}
private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) {
@@ -181,10 +183,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return detail(fulfillment.getId());
}
@OperationAudit(AuditOperation.GAO_ORDER_FULFILLMENT_PREPARE_STOCK)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_PREPARE_STOCK", request, () -> prepareStockInternal(request));
return prepareStockInternal(request);
}
private GaoOrderFulfillment prepareStockInternal(GaoOrderFulfillmentPrepareStockRequest request) {
@@ -194,12 +197,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);
@@ -216,10 +218,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return detail(fulfillment.getId());
}
@OperationAudit(AuditOperation.GAO_ORDER_FULFILLMENT_VERIFY)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment verify(GaoOrderFulfillmentActionRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_VERIFY", request, () -> verifyInternal(request));
return verifyInternal(request);
}
private GaoOrderFulfillment verifyInternal(GaoOrderFulfillmentActionRequest request) {
@@ -228,12 +231,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);
@@ -254,16 +256,18 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return detail(fulfillment.getId());
}
@OperationAudit(AuditOperation.GAO_ORDER_FULFILLMENT_REJECT)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment reject(GaoOrderFulfillmentActionRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REJECT", request, () -> releaseInternal(request, GaoOrderFulfillment.Status.REJECTED));
return releaseInternal(request, GaoOrderFulfillment.Status.REJECTED);
}
@OperationAudit(AuditOperation.GAO_ORDER_FULFILLMENT_CANCEL)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment cancel(GaoOrderFulfillmentActionRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CANCEL", request, () -> cancelInternal(request));
return cancelInternal(request);
}
private GaoOrderFulfillment cancelInternal(GaoOrderFulfillmentActionRequest request) {
@@ -283,7 +287,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
@@ -306,10 +310,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return detail(fulfillment.getId());
}
@OperationAudit(AuditOperation.GAO_ORDER_FULFILLMENT_REVOKE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment revoke(GaoOrderFulfillmentRevokeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REVOKE", request, () -> revokeInternal(request));
return revokeInternal(request);
}
private GaoOrderFulfillment revokeInternal(GaoOrderFulfillmentRevokeRequest request) {
@@ -326,7 +331,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 +361,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 +402,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 +435,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 +450,93 @@ 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));
if (requestItem.getSkuUnitId().equals(source.getSkuUnitId())) {
result.add(toSourceByOrderSnapshot(order, source, requestItem.getQuantity(), checkAvailable));
continue;
}
GaoProductSkuUnitSelection selection = skuService.getFulfillableSkuUnit(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 SourceEntry toSourceByOrderSnapshot(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) {
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);
}
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) {
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(source.getBaseQuantityPerUnitSnapshot());
BigDecimal baseQuantity = toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数");
return buildSource(order, source, source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit, quantity,
baseQuantity, product, sku, 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(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 new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, skuSpecSnapshot, unitName, baseQuantityPerUnit,
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit);
return result;
}
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 baseQuantityPerUnit = requiredBaseQuantityPerUnit(unit.getBaseQuantityPerUnit());
BigDecimal baseQuantity = toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数");
return buildSource(order, source, unit.getId(), unit.getName(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, checkAvailable);
}
private SourceEntry buildSource(GaoSalesOrder order, GaoSalesOrderItem source, String unitId, String unitName,
BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal baseQuantity, GaoProduct product, GaoProductSku sku,
boolean checkAvailable) {
BigDecimal reservedBaseBefore = decimal(source.getReservedBaseQuantity());
BigDecimal fulfilledBaseBefore = decimal(source.getFulfilledBaseQuantity());
BigDecimal totalBaseQuantity = requiredBaseQuantity(source.getBaseQuantity());
if (checkAvailable) {
BigDecimal 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 +548,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 +587,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 +605,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 +642,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 +733,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 +748,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);
@@ -1,10 +1,12 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
@@ -14,9 +16,10 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.service.AbstractEntityService;
@@ -40,12 +43,13 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
private final GaoPointTriggerService pointTriggerService;
private final AttachmentService attachmentService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
private final PermissionChecker permissionChecker;
@OperationAudit(AuditOperation.GAO_SALES_ORDER_PAYMENT_RECEIVE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord receive(GaoOrderPaymentReceiveRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_RECEIVE", request, () -> receiveInternal(request));
return receiveInternal(request);
}
private GaoFundRecord receiveInternal(GaoOrderPaymentReceiveRequest request) {
@@ -76,11 +80,11 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
return record;
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_PAYMENT_VERIFY)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord verify(String fundRecordId) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_VERIFY", fundRecordId, () -> {
GaoFundRecord record = lockOrderRecord(fundRecordId);
GaoFundRecord record = lockOrderRecord(fundRecordId);
TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态");
GaoSalesOrder order = orderService.getForUpdate(record.getBizId());
Long paidAtBefore = order.getPaidAt();
@@ -98,7 +102,6 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
pointTriggerService.onSalesOrderPaid(order);
}
return record;
});
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@@ -110,7 +113,8 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord cancel(GaoOrderPaymentActionRequest request) {
return releasePending(request, GaoFundRecord.Status.CANCELLED, true);
boolean onlyOperator = !permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.FUND_RECORD_READ.getValue());
return releasePending(request, GaoFundRecord.Status.CANCELLED, onlyOperator);
}
private GaoFundRecord releasePending(GaoOrderPaymentActionRequest request, GaoFundRecord.Status targetStatus, boolean onlyOperator) {
@@ -135,10 +139,11 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
return record;
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_REFUND)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord refund(GaoOrderPaymentRefundRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_REFUND", request, () -> refundInternal(request));
return refundInternal(request);
}
private GaoFundRecord refundInternal(GaoOrderPaymentRefundRequest request) {
@@ -172,11 +177,11 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
return record;
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_CONFIRM_NO_REFUND)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord confirmNoRefund(GaoOrderPaymentNoRefundRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CONFIRM_NO_REFUND", request, () -> {
TimiException.required(request, "not found no refund request");
TimiException.required(request, "not found no refund request");
GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId());
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
String reason = requiredText(request.getReason(), "确认无需退款原因不能为空");
@@ -194,13 +199,13 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
order.setRefundStatus(GaoSalesOrder.RefundStatus.REFUNDED);
orderService.updatePayment(order);
return record;
});
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_FUND_REVOKE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoFundRecord revoke(GaoOrderPaymentRevokeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_FUND_REVOKE", request, () -> revokeInternal(request));
return revokeInternal(request);
}
private GaoFundRecord revokeInternal(GaoOrderPaymentRevokeRequest request) {
@@ -103,20 +103,17 @@ public class GaoOrderReminderServiceImplement implements GaoOrderReminderService
log.warn("GAO 订单提醒跳过不存在的店长用户: storeId[{}], userId[{}]", storeId, manager.getUserId());
continue;
}
String remindedId = "%s:%s:%s:%s".formatted(LocalDate.now(ZoneId.of(ZONE_ID)), storeId, manager.getUserId(), reminderType);
if (notifyService.existsByBizReminded(NOTIFY_BIZ_TYPE, reminderType, remindedId)) {
continue;
}
String remindedKey = "%s:%s:%s:%s".formatted(LocalDate.now(ZoneId.of(ZONE_ID)), storeId, manager.getUserId(), reminderType);
long sendAt = Time.now();
Notify notify = new Notify();
notify.setBizType(NOTIFY_BIZ_TYPE);
notify.setBizRemindedType(reminderType);
notify.setBizRemindedId(remindedId);
notify.setBizRemindedKey(remindedKey);
notify.setDetailList(List.of(
createInternalDetail(manager.getUserId(), content, sendAt),
createSmsDetail(user.getPhoneNo(), customerNames, action, content, sendAt)
));
notifyService.create(notify);
notifyService.createIfAbsent(notify);
} catch (Exception e) {
log.error("GAO 订单提醒发送失败: storeId[{}], userId[{}], reminderType[{}]", storeId, manager.getUserId(), reminderType, e);
}
@@ -6,8 +6,8 @@ import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductSku;
import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper;
import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
@@ -44,7 +44,6 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
private final GaoPackageConfigItemMapper itemMapper;
private final GaoProductSkuService skuService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoPackageConfig, String> mapper() {
@@ -90,13 +89,11 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
.collect(java.util.stream.Collectors.groupingBy(GaoPackageConfigItem::getPackageConfigId));
}
@OperationAudit(AuditOperation.GAO_PACKAGE_CONFIG_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoPackageConfig config) {
loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_CREATE", config, () -> {
createInternal(config);
return config;
});
createInternal(config);
}
private void createInternal(GaoPackageConfig config) {
@@ -109,13 +106,11 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
saveItems(config.getId(), config.getItemList());
}
@OperationAudit(AuditOperation.GAO_PACKAGE_CONFIG_UPDATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoPackageConfig request) {
loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_UPDATE", request, () -> {
updateInternal(request);
return request;
});
updateInternal(request);
}
private void updateInternal(GaoPackageConfig request) {
@@ -141,15 +136,13 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
saveItems(dbConfig.getId(), request.getItemList());
}
@OperationAudit(AuditOperation.GAO_PACKAGE_CONFIG_DELETE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_DELETE", id, () -> {
GaoPackageConfig config = get(id);
TimiException.required(config, "not found package config");
super.delete(id);
return id;
});
GaoPackageConfig config = get(id);
TimiException.required(config, "not found package config");
super.delete(id);
}
/// 套餐配置只允许逻辑删除
@@ -68,7 +68,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
GaoPointScriptContext context = eventContext(rule, record);
GroovyScriptExecutionResult<GaoPointDecision> execution = evaluate(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(),
GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId());
if (execution == null) {
if (execution.result() == null) {
continue;
}
pointDeltaTotal += applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), null,
@@ -6,8 +6,8 @@ import com.imyeyu.api.modules.gao.mapper.GaoProductCategoryMapper;
import com.imyeyu.api.modules.gao.service.GaoProductCategoryService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
@@ -37,7 +37,6 @@ public class GaoProductCategoryServiceImplement extends AbstractEntityService<Ga
private final GaoProductCategoryMapper mapper;
private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoProductCategory, String> mapper() {
@@ -59,6 +58,7 @@ public class GaoProductCategoryServiceImplement extends AbstractEntityService<Ga
return mapper.selectByStoreId(storeId);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_CATEGORY_SORT)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void sort(List<String> idList) {
@@ -66,58 +66,50 @@ public class GaoProductCategoryServiceImplement extends AbstractEntityService<Ga
return;
}
List<String> distinctIdList = new ArrayList<>(new LinkedHashSet<>(idList));
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_SORT", distinctIdList, () -> {
Map<String, GaoProductCategory> categoryMap = mapper.selectByIdList(new LinkedHashSet<>(distinctIdList)).stream()
.collect(Collectors.toMap(GaoProductCategory::getId, Function.identity()));
TimiException.requiredTrue(categoryMap.size() == distinctIdList.size(), "产品分类列表包含不存在的分类");
for (GaoProductCategory category : categoryMap.values()) {
storeScopeService.checkStoreId(category.getStoreId());
}
mapper.updateSort(distinctIdList, Time.now());
});
Map<String, GaoProductCategory> categoryMap = mapper.selectByIdList(new LinkedHashSet<>(distinctIdList)).stream()
.collect(Collectors.toMap(GaoProductCategory::getId, Function.identity()));
TimiException.requiredTrue(categoryMap.size() == distinctIdList.size(), "产品分类列表包含不存在的分类");
for (GaoProductCategory category : categoryMap.values()) {
storeScopeService.checkStoreId(category.getStoreId());
}
mapper.updateSort(distinctIdList, Time.now());
}
@OperationAudit(AuditOperation.GAO_PRODUCT_CATEGORY_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoProductCategory category) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_CREATE", category, () -> {
prepare(category, null);
super.create(category);
return category;
});
prepare(category, null);
super.create(category);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_CATEGORY_UPDATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoProductCategory request) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_UPDATE", request, () -> {
TimiException.required(request, "not found category");
TimiException.required(request.getId(), "not found category.id");
GaoProductCategory category = get(request.getId());
TimiException.required(category, "not found category");
storeScopeService.checkStoreId(category.getStoreId());
category.setName(request.getName());
category.setSort(request.getSort());
category.setStatus(request.getStatus());
prepare(category, category.getId());
super.update(category);
return category;
});
TimiException.required(request, "not found category");
TimiException.required(request.getId(), "not found category.id");
GaoProductCategory category = get(request.getId());
TimiException.required(category, "not found category");
storeScopeService.checkStoreId(category.getStoreId());
category.setName(request.getName());
category.setSort(request.getSort());
category.setStatus(request.getStatus());
prepare(category, category.getId());
super.update(category);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_CATEGORY_DISABLE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void disable(String id) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_DISABLE", id, () -> {
GaoProductCategory category = get(id);
TimiException.required(category, "not found category");
storeScopeService.checkStoreId(category.getStoreId());
category.setStatus(GaoProductCategory.Status.INACTIVE);
category.setUpdatedAt(Time.now());
category.setSort(TimiJava.defaultIfNull(category.getSort(), 0));
super.update(category);
return category;
});
GaoProductCategory category = get(id);
TimiException.required(category, "not found category");
storeScopeService.checkStoreId(category.getStoreId());
category.setStatus(GaoProductCategory.Status.INACTIVE);
category.setUpdatedAt(Time.now());
category.setSort(TimiJava.defaultIfNull(category.getSort(), 0));
super.update(category);
}
@Override
@@ -14,9 +14,10 @@ import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductSkuService;
import com.imyeyu.api.modules.gao.service.GaoProductSpecService;
import com.imyeyu.api.modules.gao.vo.GaoProductPage;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.util.PinyinUtil;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
@@ -52,7 +53,6 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
private final ObjectProvider<GaoProductSkuService> skuServiceProvider;
private final ObjectProvider<GaoProductSpecService> specServiceProvider;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoProduct, String> mapper() {
@@ -100,62 +100,56 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
return list.stream().filter(product -> !product.getSkuList().isEmpty()).toList();
}
@OperationAudit(AuditOperation.GAO_PRODUCT_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoProduct product) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CREATE", product, () -> {
prepare(product, null);
super.create(product);
Map<String, String> specValueIdMap = saveSpecs(product, product.getSpecList());
normalizeSkuSpecValueIds(product.getSkuList(), specValueIdMap);
saveSkus(product, product.getSkuList());
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList());
return product;
});
prepare(product, null);
super.create(product);
Map<String, String> specValueIdMap = saveSpecs(product, product.getSpecList());
normalizeSkuSpecValueIds(product.getSkuList(), specValueIdMap);
saveSkus(product, product.getSkuList());
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList());
}
@OperationAudit(AuditOperation.GAO_PRODUCT_UPDATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoProduct request) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_UPDATE", request, () -> {
TimiException.required(request, "not found product");
TimiException.required(request.getId(), "not found product.id");
GaoProduct product = get(request.getId());
TimiException.required(product, "not found product");
product.setCategoryId(request.getCategoryId());
product.setCode(TimiJava.defaultIfNull(request.getCode(), product.getCode()));
product.setName(TimiJava.defaultIfNull(request.getName(), product.getName()));
product.setProductType(TimiJava.defaultIfNull(request.getProductType(), product.getProductType()));
product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled()));
product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus()));
product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort()));
product.setRemark(request.getRemark());
product.setSkuList(request.getSkuList() == null ? skuServiceProvider.getObject().listByProductId(product.getId()) : request.getSkuList());
prepare(product, product.getId());
super.update(product);
if (request.getSkuList() != null) {
saveSkus(product, request.getSkuList());
}
if (request.getAttachmentList() != null) {
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList());
}
return product;
});
TimiException.required(request, "not found product");
TimiException.required(request.getId(), "not found product.id");
GaoProduct product = get(request.getId());
TimiException.required(product, "not found product");
product.setCategoryId(request.getCategoryId());
product.setCode(TimiJava.defaultIfNull(request.getCode(), product.getCode()));
product.setName(TimiJava.defaultIfNull(request.getName(), product.getName()));
product.setProductType(TimiJava.defaultIfNull(request.getProductType(), product.getProductType()));
product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled()));
product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus()));
product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort()));
product.setRemark(request.getRemark());
product.setSkuList(request.getSkuList() == null ? skuServiceProvider.getObject().listByProductId(product.getId()) : request.getSkuList());
prepare(product, product.getId());
super.update(product);
if (request.getSkuList() != null) {
saveSkus(product, request.getSkuList());
}
if (request.getAttachmentList() != null) {
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList());
}
}
@OperationAudit(AuditOperation.GAO_PRODUCT_DISABLE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void disable(String id) {
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_DISABLE", id, () -> {
GaoProduct product = get(id);
TimiException.required(product, "not found product");
TimiException.requiredTrue(skuServiceProvider.getObject().listByProductId(id).stream().noneMatch(sku -> sku.getStatus() == com.imyeyu.api.modules.gao.entity.GaoProductSku.Status.ACTIVE), "请先停用产品的全部 SKU");
product.setStatus(GaoProduct.Status.INACTIVE);
product.setUpdatedBy(userLoginService.getRequireLoginUserId());
product.setUpdatedAt(Time.now());
super.update(product);
return product;
});
GaoProduct product = get(id);
TimiException.required(product, "not found product");
TimiException.requiredTrue(skuServiceProvider.getObject().listByProductId(id).stream().noneMatch(sku -> sku.getStatus() == com.imyeyu.api.modules.gao.entity.GaoProductSku.Status.ACTIVE), "请先停用产品的全部 SKU");
product.setStatus(GaoProduct.Status.INACTIVE);
product.setUpdatedBy(userLoginService.getRequireLoginUserId());
product.setUpdatedAt(Time.now());
super.update(product);
}
@Override
@@ -173,6 +167,7 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
TimiException.required(product.getStoreId(), "not found product.storeId");
product.setCode(requiredText(product.getCode(), "产品编码不能为空"));
product.setName(requiredText(product.getName(), "产品名称不能为空"));
fillPinyin(product);
product.setProductType(TimiJava.defaultIfNull(product.getProductType(), GaoProduct.ProductType.GOODS));
product.setStockEnabled(TimiJava.defaultIfNull(product.getStockEnabled(), product.getProductType() != GaoProduct.ProductType.SERVICE));
TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存");
@@ -268,4 +263,14 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
/// 填充产品名称拼音搜索字段
///
/// @param product 产品
private void fillPinyin(GaoProduct product) {
String name = product.getName();
product.setNamePinyinFull(PinyinUtil.full(name));
product.setNamePinyinInitial(PinyinUtil.initial(name));
product.setNamePinyinMixed(PinyinUtil.mixed(name));
}
}
@@ -76,6 +76,16 @@ public class GaoProductSkuServiceImplement extends AbstractEntityService<GaoProd
return sku;
}
@Override
public List<GaoProductSku> listByIdList(Collection<String> idList) {
if (idList == null || idList.isEmpty()) {
return List.of();
}
List<GaoProductSku> result = mapper.selectByIdList(idList);
fillTransient(result);
return result;
}
@Override
public List<GaoProductSku> listByProductId(String productId) {
TimiException.required(productId, "not found productId");
@@ -99,6 +109,22 @@ public class GaoProductSkuServiceImplement extends AbstractEntityService<GaoProd
return getSkuUnit(storeId, skuId, skuUnitId, false);
}
@Override
public GaoProductSkuUnitSelection getFulfillableSkuUnit(String storeId, String skuId, String skuUnitId) {
TimiException.required(storeId, "门店 ID 不能为空");
TimiException.required(skuId, "SKU ID 不能为空");
TimiException.required(skuUnitId, "SKU 单位 ID 不能为空");
GaoProductSku sku = mapper.selectForUpdate(skuId);
TimiException.required(sku, "SKU 不存在");
TimiException.requiredTrue(storeId.equals(sku.getStoreId()), "SKU 不属于当前门店");
GaoProduct product = productServiceProvider.getObject().get(sku.getProductId());
TimiException.required(product, "产品不存在");
GaoProductSkuUnit unit = unitMapper.selectForUpdate(skuUnitId);
TimiException.required(unit, "SKU 单位不存在");
TimiException.requiredTrue(skuId.equals(unit.getSkuId()), "SKU 单位不属于当前 SKU");
return new GaoProductSkuUnitSelection(product, sku, unit);
}
@Override
public GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase) {
TimiException.required(storeId, "not found storeId");
@@ -344,11 +370,8 @@ public class GaoProductSkuServiceImplement extends AbstractEntityService<GaoProd
TimiException.requiredTrue(1 == unitMapper.updateUnit(unit), "SKU 单位更新失败");
}
}
for (GaoProductSkuUnit existing : existingUnitList) {
if (!retainedIdSet.contains(existing.getId())) {
unitMapper.delete(existing.getId());
}
}
TimiException.requiredTrue(existingUnitList.stream().allMatch(existing -> retainedIdSet.contains(existing.getId())),
"已保存的 SKU 单位不能删除,请关闭允许销售和允许采购");
}
private void saveSpecValues(String skuId, List<String> specValueIdList) {
@@ -4,24 +4,24 @@ import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.entity.GaoProductSku;
import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit;
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord;
import com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductStockService;
import com.imyeyu.api.modules.gao.service.GaoProductSkuService;
import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoProductStockService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
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;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
@@ -59,7 +59,6 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
private final GaoProductSkuService skuService;
private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
public PageResult<GaoProductStock> pageByQuery(GaoProductStockPage page) {
@@ -112,40 +111,46 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
return stock;
}
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_INITIAL_IN)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock initialIn(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_INITIAL_IN", request, () -> change(request, GaoProductStockRecord.OperationType.INITIAL_IN));
return change(request, GaoProductStockRecord.OperationType.INITIAL_IN);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_IN)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock manualIn(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_IN", request, () -> change(request, GaoProductStockRecord.OperationType.MANUAL_IN));
return change(request, GaoProductStockRecord.OperationType.MANUAL_IN);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_OUT)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock manualOut(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_OUT", request, () -> change(request, GaoProductStockRecord.OperationType.MANUAL_OUT));
return change(request, GaoProductStockRecord.OperationType.MANUAL_OUT);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_REVOKE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock manualRevoke(GaoProductStockManualRevokeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_REVOKE", request, () -> manualRevokeInternal(request));
return manualRevokeInternal(request);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_STOCKTAKE_GAIN)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock stocktakeGain(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_GAIN", request, () -> change(request, GaoProductStockRecord.OperationType.STOCKTAKE_GAIN));
return change(request, GaoProductStockRecord.OperationType.STOCKTAKE_GAIN);
}
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_STOCKTAKE_LOSS)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoProductStock stocktakeLoss(GaoProductStockChangeRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_LOSS", request, () -> change(request, GaoProductStockRecord.OperationType.STOCKTAKE_LOSS));
return change(request, GaoProductStockRecord.OperationType.STOCKTAKE_LOSS);
}
@Override
@@ -216,7 +221,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
GaoStockChangeItem item = GaoStockChangeItem.of(product, sku, unit, quantity, baseQuantity);
item.setSkuSpecSnapshot(skuService.getSkuSpecSnapshot(sku));
List<GaoProductStock> result = executeInternal(storeId, sku.getId(), List.of(item), requestNo,
userLoginService.getRequireLoginUserId(), operationType, request.getRemark(), request.getAttachmentList());
userLoginService.getRequireLoginUserId(), operationType, request.getOccurredAt(), request.getRemark(), request.getAttachmentList());
return result.getFirst();
}
@@ -295,11 +300,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
private List<GaoProductStock> executeInternal(String storeId, String bizId, List<GaoStockChangeItem> sourceList, String requestNo,
String operatorUserId, GaoProductStockRecord.OperationType operationType, String remark) {
return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, remark, null);
return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, null, remark, null);
}
private List<GaoProductStock> executeInternal(String storeId, String bizId, List<GaoStockChangeItem> sourceList, String requestNo,
String operatorUserId, GaoProductStockRecord.OperationType operationType, String remark, List<Attachment> attachmentList) {
String operatorUserId, GaoProductStockRecord.OperationType operationType, Long occurredAt, String remark, List<Attachment> attachmentList) {
TimiException.required(storeId, "not found storeId");
TimiException.required(bizId, "not found stock bizId");
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "库存变更至少需要一个产品");
@@ -360,7 +365,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
record.setSourceRecordId(item.getSourceRecordId());
record.setRequestNo(normalizedRequestNo);
record.setOperatorUserId(operatorUserId);
record.setOccurredAt(Time.now());
record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now()));
record.setRemark(remark);
recordService.createOperationRecord(record, attachmentList);
stock.setOnHandQuantity(onHandAfter);
@@ -34,8 +34,8 @@ import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderUpdateRequest;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
@@ -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;
@@ -88,7 +91,6 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
private final PermissionChecker permissionChecker;
private final RoleChecker roleChecker;
private final UserLoginService userLoginService;
private final LoggerService loggerService;
@Override
protected BaseMapper<GaoSalesOrder, String> mapper() {
@@ -113,6 +115,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
TimiException.required(order, "not found sales order");
storeScopeService.checkStoreId(order.getStoreId());
List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId());
fillItemDisplayInfo(itemList);
for (GaoSalesOrderItem item : itemList) {
fillItemBalance(item);
}
@@ -136,23 +139,23 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
return order;
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE", request, () -> createDirectInternal(request));
return createDirectInternal(request);
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_UPDATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoSalesOrder updateRemarkAndAttachments(GaoSalesOrderUpdateRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_UPDATE", request, () -> {
TimiException.required(request, "not found sales order update request");
TimiException.required(request.getId(), "not found sales order.id");
GaoSalesOrder order = getForUpdate(request.getId());
TimiException.requiredTrue(1 == mapper.updateRemark(order.getId(), request.getRemark()), "订单备注更新失败");
attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId(), request.getAttachmentList());
return detail(order.getId());
});
TimiException.required(request, "not found sales order update request");
TimiException.required(request.getId(), "not found sales order.id");
GaoSalesOrder order = getForUpdate(request.getId());
TimiException.requiredTrue(1 == mapper.updateRemark(order.getId(), request.getRemark()), "订单备注更新失败");
attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId(), request.getAttachmentList());
return detail(order.getId());
}
private GaoSalesOrder createDirectInternal(GaoDirectProductOrderCreateRequest request) {
@@ -219,10 +222,11 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
data.getSaleAmount(), data.getItemList());
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_CANCEL)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoSalesOrder cancel(GaoSalesOrderCancelRequest request) {
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CANCEL", request, () -> cancelInternal(request));
return cancelInternal(request);
}
private GaoSalesOrder cancelInternal(GaoSalesOrderCancelRequest request) {
@@ -280,18 +284,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()), "订单未有已核销收款,不能履约");
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
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 +316,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 +390,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));
@@ -445,20 +453,25 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
for (GaoSalesOrderItem item : itemList) {
TimiException.required(item, "销售订单明细不能为空");
TimiException.requiredTrue(item.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
item.setGifted(Boolean.TRUE.equals(item.getGifted()));
TimiException.required(item.getSkuId(), "订单项 SKU 不能为空");
TimiException.required(item.getSkuUnitId(), "订单项 SKU 单位不能为空");
TimiException.requiredTrue(skuUnitKeySet.add(item.getSkuId() + "|" + item.getSkuUnitId()),
String skuUnitKey = item.getSkuId() + "|" + item.getSkuUnitId() + "|" + item.getGifted();
TimiException.requiredTrue(skuUnitKeySet.add(skuUnitKey),
"同一订单不能重复 SKU 和单位");
TimiException.required(item.getQuantity(), "订单项数量不能为空");
TimiException.requiredTrue(0 < item.getQuantity().signum(), "订单项数量必须大于 0");
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(), "套餐订单项不能填写单价");
@@ -500,11 +513,14 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
List<GaoSalesOrderItem> existingItemList = itemMapper.selectBySalesOrderId(existing.getId());
TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应订单内容不一致");
Map<String, GaoSalesOrderItem> existingItemMap = existingItemList.stream()
.collect(java.util.stream.Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), java.util.function.Function.identity()));
.collect(java.util.stream.Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId() + "|" + Boolean.TRUE.equals(item.getGifted()),
java.util.function.Function.identity()));
for (GaoSalesOrderItem requestItem : requestItemList) {
GaoSalesOrderItem existingItem = existingItemMap.get(requestItem.getSkuId() + "|" + requestItem.getSkuUnitId());
GaoSalesOrderItem existingItem = existingItemMap.get(requestItem.getSkuId() + "|" + requestItem.getSkuUnitId() + "|"
+ Boolean.TRUE.equals(requestItem.getGifted()));
TimiException.required(existingItem, "requestNo 对应订单内容不一致");
TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT
&& Boolean.TRUE.equals(existingItem.getGifted()) == Boolean.TRUE.equals(requestItem.getGifted())
&& existingItem.getQuantity().compareTo(requestItem.getQuantity()) == 0
&& existingItem.getSort().equals(requestItem.getSort())
&& sameText(existingItem.getRemark(), requestItem.getRemark()), "requestNo 对应订单内容不一致");
@@ -544,12 +560,43 @@ 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));
}
/// 填充订单项数量展示所需的 SKU 信息
///
/// @param itemList 订单项列表
private void fillItemDisplayInfo(List<GaoSalesOrderItem> itemList) {
if (itemList == null || itemList.isEmpty()) {
return;
}
List<String> skuIdList = itemList.stream()
.map(GaoSalesOrderItem::getSkuId)
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
if (skuIdList.isEmpty()) {
return;
}
Map<String, GaoProductSku> skuMap = skuService.listByIdList(skuIdList).stream()
.collect(java.util.stream.Collectors.toMap(GaoProductSku::getId, java.util.function.Function.identity()));
for (GaoSalesOrderItem item : itemList) {
GaoProductSku sku = skuMap.get(item.getSkuId());
if (sku == null) {
continue;
}
item.setAllowDecimalQuantity(sku.getAllowDecimalQuantity());
item.setBaseUnitName(sku.getUnitList().stream()
.filter(unit -> Boolean.TRUE.equals(unit.getIsBase()))
.map(GaoProductSkuUnit::getName)
.findFirst()
.orElse(null));
}
}
private void fillItemAttachments(List<GaoSalesOrderItem> itemList) {
@@ -606,6 +653,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();
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
@@ -43,6 +44,12 @@ public class GaoStoreChartServiceImplement implements GaoStoreChartService {
return customerService.stateGender(storeId);
}
@Override
public List<GaoCustomerAgeStatView> customerAge(String storeId) {
TimiException.required(storeId, "not found storeId");
return customerService.stateAge(storeId);
}
@Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req) {
checkRangeReq(req);
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户年龄统计视图
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerAgeStatView {
/// 客户年龄
private Long age;
/// 该年龄的客户数量
private Long count;
}
@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.gao.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
///
/// 客户每日统计中的客户摘要
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerDailyStateCustomerView {
/// 用于服务层按日期分组,不返回给前端
@JsonIgnore
private Integer dateValue;
/// 客户姓名
private String name;
}
@@ -2,6 +2,8 @@ package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.util.List;
///
/// 客户每日统计视图
///
@@ -18,4 +20,7 @@ public class GaoCustomerDailyStateView {
/// 截止当日客户总数
private Long totalCustomerCount;
/// 当日新增客户列表
private List<GaoCustomerDailyStateCustomerView> customerList;
}
@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// 客户病症原始文本
///
/// 只用于后端分词,不对外返回。
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerDiseaseTextView {
/// 备注
private String remark;
/// 病症
private String disease;
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// 客户病症词频查询请求
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerDiseaseWordReq {
/// 门店 ID
private String storeId;
/// 返回词语数量
private Integer topN;
}
@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
/// 客户病症词频
///
/// @author Codex
/// @since 2026-09-11
@Data
@AllArgsConstructor
public class GaoCustomerDiseaseWordView {
/// 词语
private String word;
/// 词语出现次数
private Long value;
}
@@ -17,6 +17,9 @@ public class GaoCustomerPackagePurchaseItem {
/// SKU 单位 ID
private String skuUnitId;
/// true 表示请求将该明细作为赠送权益创建
private Boolean gifted;
/// 产品名称快照,由后端生成
private String productNameSnapshot;
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.java.bean.BasePage;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.spring.bean.Page;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -12,7 +13,7 @@ import java.util.List;
/// @since 2026-08-13
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoCustomerPage extends BasePage {
public class GaoCustomerPage extends Page<GaoCustomer> {
/// 门店 ID
private String storeId;
@@ -22,4 +23,10 @@ public class GaoCustomerPage extends BasePage {
/// 标签 ID 列表,客户必须同时拥有列表内全部标签
private List<String> tagIdList;
/// 录入时间起始,包含,Unix 毫秒时间戳
private Long beginCreatedAt;
/// 录入时间结束,包含,Unix 毫秒时间戳
private Long endCreatedAt;
}
@@ -39,4 +39,7 @@ public class GaoFundRecordPage extends BasePage {
/// 操作结束时间
private Long endAt;
/// 操作用户 ID,仅限服务端设置的数据范围条件
private String operatorUserId;
}
@@ -14,6 +14,9 @@ public class GaoOrderFulfillmentItemRequest {
/// 来源销售订单明细 ID
private String sourceOrderItemId;
/// 本次实际履约 SKU 单位 ID
private String skuUnitId;
/// 本次履约数量
private BigDecimal quantity;
}
@@ -30,6 +30,6 @@ public class GaoProductPage extends BasePage {
/// 产品类型
private GaoProduct.ProductType productType;
/// 名称或编码关键词
/// 名称、拼音或编码关键词
private String keyword;
}
@@ -28,6 +28,9 @@ public class GaoProductStockChangeRequest {
/// 业务数量
private BigDecimal quantity;
/// 库存发生时间,毫秒时间戳
private Long occurredAt;
/// 备注
private String remark;
@@ -18,6 +18,6 @@ public class GaoProductStockPage extends BasePage {
/// 产品 ID
private String productId;
/// 产品或 SKU 名称、编码、规格摘要关键词
/// 产品或 SKU 名称、拼音、编码、规格摘要关键词
private String keyword;
}
@@ -19,7 +19,7 @@ public class GaoProductStockRecordPage extends BasePage {
/// 产品 ID
private String productId;
/// 产品名称或编码关键词
/// 产品名称、拼音或编码关键词
private String keyword;
/// 来源业务类型
@@ -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);
}
@@ -0,0 +1,51 @@
package com.imyeyu.api.modules.system.audit;
import com.imyeyu.api.modules.system.entity.Logger;
import lombok.AllArgsConstructor;
import lombok.Getter;
/// 操作审计编码
///
/// @author Codex
/// @since 2026-09-09
@Getter
@AllArgsConstructor
public enum AuditOperation {
COMMON_NOTIFY_CREATE(Logger.Module.COMMON),
COMMON_NOTIFY_DELETE(Logger.Module.COMMON),
GAO_CUSTOMER_PACKAGE_PURCHASE(Logger.Module.GAO),
GAO_CUSTOMER_PACKAGE_CANCEL(Logger.Module.GAO),
GAO_SALES_ORDER_PAYMENT_RECEIVE(Logger.Module.GAO),
GAO_SALES_ORDER_PAYMENT_VERIFY(Logger.Module.GAO),
GAO_SALES_ORDER_REFUND(Logger.Module.GAO),
GAO_SALES_ORDER_CONFIRM_NO_REFUND(Logger.Module.GAO),
GAO_SALES_ORDER_FUND_REVOKE(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_CREATE(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_PREPARE_STOCK(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_VERIFY(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_REJECT(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_CANCEL(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_REVOKE(Logger.Module.GAO),
GAO_PACKAGE_CONFIG_CREATE(Logger.Module.GAO),
GAO_PACKAGE_CONFIG_UPDATE(Logger.Module.GAO),
GAO_PACKAGE_CONFIG_DELETE(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_SORT(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_CREATE(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_UPDATE(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_DISABLE(Logger.Module.GAO),
GAO_PRODUCT_CREATE(Logger.Module.GAO),
GAO_PRODUCT_UPDATE(Logger.Module.GAO),
GAO_PRODUCT_DISABLE(Logger.Module.GAO),
GAO_PRODUCT_STOCK_INITIAL_IN(Logger.Module.GAO),
GAO_PRODUCT_STOCK_MANUAL_IN(Logger.Module.GAO),
GAO_PRODUCT_STOCK_MANUAL_OUT(Logger.Module.GAO),
GAO_PRODUCT_STOCK_MANUAL_REVOKE(Logger.Module.GAO),
GAO_PRODUCT_STOCK_STOCKTAKE_GAIN(Logger.Module.GAO),
GAO_PRODUCT_STOCK_STOCKTAKE_LOSS(Logger.Module.GAO),
GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE(Logger.Module.GAO),
GAO_SALES_ORDER_UPDATE(Logger.Module.GAO),
GAO_SALES_ORDER_CANCEL(Logger.Module.GAO);
final Logger.Module module;
}
@@ -0,0 +1,128 @@
package com.imyeyu.api.modules.system.audit;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.spring.entity.IDEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.lang.reflect.Array;
import java.util.Locale;
import java.util.Set;
/// 操作审计参数和结果序列化器
///
/// @author Codex
/// @since 2026-09-09
@Component
@RequiredArgsConstructor
public class AuditPayloadSerializer {
private static final int MAX_LENGTH = 32 * 1024;
private static final Set<String> SECRET_FIELD_SET = Set.of(
"password",
"passwordconfirm",
"token",
"accesstoken",
"refreshtoken",
"secret",
"credential",
"authorization"
);
private final ObjectMapper objectMapper;
/// 序列化方法参数
///
/// @param arguments 方法参数
/// @return 脱敏后的参数 JSON
public String serializeArguments(Object[] arguments) {
if (arguments == null || 0 == arguments.length) {
return null;
}
return serialize(arguments.length == 1 ? arguments[0] : arguments);
}
/// 序列化业务结果
///
/// @param result 方法返回值
/// @param arguments 方法参数
/// @return 结果摘要
public String serializeResult(Object result, Object[] arguments) {
if (result instanceof IDEntity<?> entity && entity.getId() != null) {
return truncate(String.valueOf(entity.getId()));
}
if (result != null) {
return summarize(result);
}
if (arguments != null) {
for (Object argument : arguments) {
if (argument instanceof IDEntity<?> entity && entity.getId() != null) {
return truncate(String.valueOf(entity.getId()));
}
}
if (1 == arguments.length && arguments[0] instanceof CharSequence value) {
return truncate(value.toString());
}
}
return null;
}
private String serialize(Object value) {
try {
JsonNode node = objectMapper.valueToTree(value);
mask(node);
return truncate(objectMapper.writeValueAsString(node));
} catch (Exception e) {
return value == null ? null : truncate(value.getClass().getName());
}
}
private void mask(JsonNode node) {
if (node == null) {
return;
}
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
objectNode.fieldNames().forEachRemaining(fieldName -> {
JsonNode field = objectNode.get(fieldName);
if (SECRET_FIELD_SET.contains(fieldName.toLowerCase(Locale.ROOT))) {
objectNode.put(fieldName, "***");
} else {
mask(field);
}
});
} else if (node.isArray()) {
node.forEach(this::mask);
}
}
private String summarize(Object value) {
if (value instanceof CharSequence || value instanceof Number || value instanceof Boolean || value instanceof Enum<?>) {
return truncate(String.valueOf(value));
}
if (value instanceof Iterable<?> iterable) {
int size = 0;
for (Object ignored : iterable) {
size++;
}
return "%s[%s]".formatted(value.getClass().getSimpleName(), size);
}
if (value instanceof java.util.Map<?, ?> map) {
return "%s[%s]".formatted(value.getClass().getSimpleName(), map.size());
}
if (value.getClass().isArray()) {
return "%s[%s]".formatted(value.getClass().getComponentType().getSimpleName(), Array.getLength(value));
}
return value.getClass().getSimpleName();
}
private String truncate(String value) {
if (value == null || value.length() <= MAX_LENGTH) {
return value;
}
return value.substring(0, MAX_LENGTH) + "...";
}
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.system.audit;
import com.imyeyu.api.modules.system.entity.Logger;
/// 操作审计记录器
///
/// 业务切面只依赖该写入端口,不直接依赖日志查询服务
///
/// @author Codex
/// @since 2026-09-09
public interface AuditRecorder {
/// 记录操作审计
///
/// @param logger 审计记录
void record(Logger logger);
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.system.audit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/// 标记需要记录操作审计的应用服务方法
///
/// 只应标记对外的业务操作入口,不要标记被入口调用的内部方法
///
/// @author Codex
/// @since 2026-09-09
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationAudit {
/// 操作编码
///
/// @return 操作编码
AuditOperation value();
}
@@ -0,0 +1,83 @@
package com.imyeyu.api.modules.system.audit;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/// 操作审计切面
///
/// 该切面放在事务拦截器外层,确保成功日志只在事务提交成功后生成
///
/// @author Codex
/// @since 2026-09-09
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 100)
@RequiredArgsConstructor
@Slf4j
public class OperationAuditAspect {
private final AuditRecorder auditRecorder;
private final AuditPayloadSerializer payloadSerializer;
/// 执行并记录操作审计
///
/// @param joinPoint 连接点
/// @param operationAudit 审计配置
/// @return 业务返回值
/// @throws Throwable 业务异常
@Around("@annotation(operationAudit)")
public Object around(ProceedingJoinPoint joinPoint, OperationAudit operationAudit) throws Throwable {
AuditOperation operation = operationAudit.value();
Object[] arguments = joinPoint.getArgs();
Logger logger = new Logger(operation.getModule(), operation.name());
logger.setContent(payloadSerializer.serializeArguments(arguments));
long startNanos = System.nanoTime();
try {
Object result = joinPoint.proceed();
logger.setLevel(Logger.Level.INFO);
logger.setResult(payloadSerializer.serializeResult(result, arguments));
logger.setDuration(duration(startNanos));
record(logger);
return result;
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
logger.setDuration(duration(startNanos));
record(logger);
throw e;
} catch (Throwable e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
logger.setDuration(duration(startNanos));
log.error("业务操作失败: {}", operation.name(), e);
record(logger);
throw e;
}
}
private void record(Logger logger) {
try {
auditRecorder.record(logger);
} catch (Exception e) {
log.error("记录操作审计失败: {}", logger.getOperation(), e);
}
}
private int duration(long startNanos) {
long millis = (System.nanoTime() - startNanos) / 1_000_000L;
if (Integer.MAX_VALUE < millis) {
return Integer.MAX_VALUE;
}
return (int) Math.max(0L, millis);
}
}
@@ -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,19 +0,0 @@
package com.imyeyu.api.modules.system.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
/**
*
*
* @author 夜雨
* @since 2026-04-23 16:31
*/
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/system/setting")
public class SettingController {
}
@@ -6,8 +6,6 @@ import com.imyeyu.spring.service.DestroyableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.PageableService;
import java.util.function.Supplier;
/**
* 日志服务
*
@@ -18,51 +16,6 @@ public interface LoggerService extends GettableService<Logger, String>, Pageable
void createSync(Logger logger);
/// 执行并记录有返回值的业务操作
///
/// @param logger 日志对象,调用方负责设置模块、操作和请求内容
/// @param action 业务操作
/// @param <T> 返回值类型
/// @return 业务操作返回值
<T> T execute(Logger logger, Supplier<T> action);
/// 执行并记录无返回值的业务操作
///
/// @param logger 日志对象,调用方负责设置模块、操作和请求内容
/// @param action 业务操作
default void execute(Logger logger, Runnable action) {
execute(logger, () -> {
action.run();
return null;
});
}
/// 创建日志并执行有返回值的业务操作
///
/// @param module 日志模块
/// @param operation 操作描述
/// @param content 请求内容
/// @param action 业务操作
/// @param <T> 返回值类型
/// @return 业务操作返回值
default <T> T execute(Logger.Module module, String operation, Object content, Supplier<T> action) {
Logger logger = new Logger(module, operation);
logger.setContent(content == null ? null : content.toString());
return execute(logger, action);
}
/// 创建日志并执行无返回值的业务操作
///
/// @param module 日志模块
/// @param operation 操作描述
/// @param content 请求内容
/// @param action 业务操作
default void execute(Logger.Module module, String operation, Object content, Runnable action) {
Logger logger = new Logger(module, operation);
logger.setContent(content == null ? null : content.toString());
execute(logger, action);
}
void create(Logger.Module module, Logger.Level level, String operation, String content, String result);
default void info(Logger.Module module, String operation) {
@@ -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,37 +1,41 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.audit.AuditRecorder;
import com.imyeyu.api.modules.system.mapper.LoggerMapper;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.entity.IDEntity;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.function.Supplier;
/**
* 日志服务实现
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class LoggerServiceImplement extends AbstractEntityService<Logger, String> implements LoggerService {
public class LoggerServiceImplement extends AbstractEntityService<Logger, String> implements LoggerService, AuditRecorder {
private final UserLoginService userLoginService;
private final LoggerMapper mapper;
private final ThreadPoolTaskExecutor auditTaskExecutor;
public LoggerServiceImplement(UserLoginService userLoginService, LoggerMapper mapper,
@Qualifier("auditTaskExecutor") ThreadPoolTaskExecutor auditTaskExecutor) {
this.userLoginService = userLoginService;
this.mapper = mapper;
this.auditTaskExecutor = auditTaskExecutor;
}
@Override
protected BaseMapper<Logger, String> mapper() {
return mapper;
@@ -39,9 +43,13 @@ public class LoggerServiceImplement extends AbstractEntityService<Logger, String
@Override
public void create(Logger logger) {
// 默认异步
fillContext(logger);
Thread.startVirtualThread(() -> createSync(logger));
auditTaskExecutor.execute(() -> createSync(logger));
}
@Override
public void record(Logger logger) {
create(logger);
}
@Override
@@ -50,32 +58,6 @@ public class LoggerServiceImplement extends AbstractEntityService<Logger, String
super.create(logger);
}
@Override
public <T> T execute(Logger logger, Supplier<T> action) {
try {
T result = action.get();
logger.setLevel(Logger.Level.INFO);
if (result instanceof IDEntity<?> entity) {
logger.setResult(String.valueOf(entity.getId()));
} else if (result != null) {
logger.setResult(result.toString());
}
create(logger);
return result;
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
create(logger);
throw e;
} catch (RuntimeException e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("业务操作失败: {}", logger.getOperation(), e);
create(logger);
throw e;
}
}
public void create(Logger.Module module, Logger.Level level, String operation, String content, String result) {
Logger log = new Logger(module, level, operation);
log.setContent(content);
@@ -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();

Some files were not shown because too many files have changed in this diff Show More